Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134// E2E for code-linked diagrams: links set by an agent (add_elements `code`) and by a human// (Code tab), browsing code from the diagram, and `drawgent check` (+ --fix) after the code// moves, gets renamed or deleted.// node scripts/code-e2e.mjs [path/to/drawgent]import { execFileSync, spawn } from "node:child_process";import fs from "node:fs";import os from "node:os";import path from "node:path";import { chromium } from "playwright-core";
const BIN = path.resolve(process.argv[2] || "./target/release/drawgent");const PORT = 7380, API = `http://127.0.0.1:${PORT}`;const ws = fs.mkdtempSync(path.join(os.tmpdir(), "dg-code-"));const diagram = path.join(ws, "drawgent.excalidraw");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 j = await r.json(); if (!r.ok) throw new Error(`${method} ${p}: ${j.error}`); return j; });const write = (f, s) => (fs.mkdirSync(path.dirname(path.join(ws, f)), { recursive: true }), fs.writeFileSync(path.join(ws, f), s));const check = (args = []) => { try { return { code: 0, out: execFileSync(BIN, ["check", "--root", ws, diagram, ...args], { encoding: "utf8" }) }; } catch (e) { return { code: e.status, out: String(e.stdout) }; }};const checks = [];const ok = (name, cond, extra = "") => { checks.push(!!cond); console.log(`${cond ? "✓" : "✗"} ${name}${extra ? ` (${extra})` : ""}`); };
write("src/api.rs", "use crate::db::Pool;\n\n/// Entry point for HTTP requests.\npub fn handle_request(pool: &Pool) -> u16 {\n 200\n}\n");write("src/db.rs", "pub struct Pool {\n size: usize,\n}\n\nimpl Pool {\n pub fn get(&self) {}\n}\n");execFileSync("git", ["-C", ws, "init", "-q"]);execFileSync("git", ["-C", ws, "add", "-A"]);
const srv = spawn(BIN, ["serve", "--port", String(PORT), "--workdir", ws, "--data", path.join(ws, ".drawgent/scene.json"), "--diagram", diagram], { cwd: ws, stdio: ["ignore", "ignore", "inherit"] });let browser;try { for (let i = 0; i < 40 && !(await api("/api/health").catch(() => null)); i++) await sleep(250);
// An agent draws the repo with links (add_elements `code`). await api("/api/elements", { elements: [ { type: "rectangle", id: "api", x: 0, y: 0, width: 180, height: 70, label: { text: "API" }, code: { path: "src/api.rs", line: 4, symbol: "handle_request" } }, { type: "rectangle", id: "db", x: 300, y: 0, width: 180, height: 70, label: { text: "DB" } }, { type: "arrow", x: 0, y: 0, start: { id: "api" }, end: { id: "db" } }, ] }); let links = (await api("/api/code/links")).links; ok("agent-set link is ok", links.length === 1 && links[0].status === "ok" && links[0].label === "API", JSON.stringify(links[0]?.code));
// A human links DB from the Code tab: open src/db.rs, select line 1, select the DB shape, Link. 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: 1600, height: 900 } }); await page.goto(API); await page.waitForFunction(() => window.drawgentEditor?.getSceneElements().length >= 4); await page.click("text=</> Code"); await page.click(".dg-tree [data-ref-path='src/'] .dg-tree-name"); // expand src/ in the tree await page.click(".dg-tree [data-ref-path='src/db.rs'] .dg-tree-name"); await page.waitForSelector(".dg-code-body [data-line='1']"); await page.click(".dg-code-body [data-line='1'] .dg-ln"); await page.evaluate(() => window.drawgentEditor.updateScene({ appState: { selectedElementIds: { db: true } } })); await page.waitForSelector("text=Link the selected shape"); const label = await page.textContent(".dg-code-actions .dg-primary"); await page.click(".dg-code-actions .dg-primary"); await sleep(1500); links = (await api("/api/code/links")).links; const db = links.find((l) => l.id === "db"); ok("human links a shape from the Code tab (symbol detected)", db?.code.path === "src/db.rs" && db.code.symbol === "Pool", label.trim()); const chip = await page.textContent(".dg-code-shapes").catch(() => ""); ok("file view lists the shapes linked to it", chip.includes("DB"), chip.trim());
// Code → canvas: the "Linked on this board" list opens the code; chips reveal the shape. await page.click(".dg-code-head button"); // back await page.click(".dg-links >> text=src/api.rs:4"); await page.waitForSelector(".dg-line-sel[data-line='4']"); ok("link list opens the code at the linked line", true); await page.click(".dg-code-shapes >> text=API"); await sleep(500); const selected = await page.evaluate(() => Object.keys(window.drawgentEditor.getAppState().selectedElementIds)); ok("chip reveals and selects the shape on the canvas", selected.includes("api"), selected.join(",")); // Canvas → code: clicking the link icon of a shape opens the Code tab at its line. await page.click("text=💬 Chat"); await page.evaluate(() => window.drawgentEditor.updateScene({ appState: { selectedElementIds: {} } })); const icon = await page.evaluate(() => { const s = window.drawgentEditor.getAppState(); const el = window.drawgentEditor.getSceneElements().find((e) => e.id === "api"); const sx = (x) => (x + s.scrollX) * s.zoom.value + s.offsetLeft; const sy = (y) => (y + s.scrollY) * s.zoom.value + s.offsetTop; return { x: sx(el.x + el.width) + 7, y: sy(el.y) - 8, hx: sx(el.x + el.width / 2), hy: sy(el.y + el.height / 2) }; }); await page.mouse.move(icon.hx, icon.hy); // hover the shape so its link icon is active await sleep(300); await page.mouse.move(icon.x, icon.y); await sleep(200); await page.mouse.click(icon.x, icon.y); const opened = await page.waitForSelector(".dg-line-sel[data-line='4']", { timeout: 4000 }).then(() => true).catch(() => false); const openedPath = opened ? await page.textContent(".dg-code-path") : ""; ok("clicking a shape's link icon opens its code at the line", opened && openedPath.includes("src/api.rs"), openedPath); await sleep(1500); // let the diagram file be written
// The code moves: two lines inserted above handle_request. let r = check(); ok("check passes on fresh links", r.code === 0, r.out.trim().split("\n")[0]); write("src/api.rs", "use crate::db::Pool;\nuse std::io;\n// new comment\n\n/// Entry point for HTTP requests.\npub fn handle_request(pool: &Pool) -> u16 {\n 200\n}\n"); r = check(); ok("check flags the moved symbol (exit 1)", r.code === 1 && r.out.includes("moved") && r.out.includes("line 6"), r.out.trim().split("\n").slice(1).join(" | ")); r = check(["--fix"]); const fixedLine = JSON.parse(fs.readFileSync(diagram, "utf8")).elements.find((e) => e.id === "api").customData.drawgentCode.line; ok("check --fix repairs the line in the diagram file", r.code === 0 && fixedLine === 6, `line ${fixedLine}`);
// A file is renamed. fs.mkdirSync(path.join(ws, "src/storage")); fs.renameSync(path.join(ws, "src/db.rs"), path.join(ws, "src/storage/db.rs")); r = check(); ok("check flags the renamed file", r.code === 1 && r.out.includes("missing") && r.out.includes("src/storage/db.rs"), r.out.trim().split("\n").slice(1).join(" | ")); r = check(["--fix"]); ok("check --fix follows the rename", r.code === 0);
// A symbol disappears. write("src/api.rs", "pub fn serve() {}\n"); r = check(); ok("check flags a deleted symbol as stale", r.code === 1 && r.out.includes("stale") && r.out.includes("no longer in src/api.rs"), r.out.trim().split("\n").slice(1).join(" | ")); await page.screenshot({ path: "data/code-e2e.png" });} finally { await browser?.close(); srv.kill("SIGINT"); await sleep(500); fs.rmSync(ws, { recursive: true, force: true });}const all = checks.every(Boolean);console.log(all ? "CODE E2E OK" : "CODE E2E FAILED");process.exit(all ? 0 : 1);