Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129// E2E for `drawgent up -d` / `status` / `down` and canvas-only mode (`--no-workspace`), with a// throwaway config whose "claude" is the fake Claude Code CLI behind the built-in bridge.// node scripts/daemon-e2e.mjs [path/to/drawgent]import { spawnSync, execFileSync } 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 WebSocket from "ws";
const here = path.dirname(fileURLToPath(import.meta.url));const BIN = path.resolve(process.argv[2] || "./target/release/drawgent");const FAKE = path.join(here, "fake-claude.mjs");fs.chmodSync(FAKE, 0o755);const PORT = 7389, API = `http://127.0.0.1:${PORT}`;const root = fs.mkdtempSync(path.join(os.tmpdir(), "dg-daemon-"));const LOG = path.join(root, "fake-claude.jsonl");const env = { ...process.env, DRAWGENT_CONFIG: path.join(root, "config.toml"), XDG_CACHE_HOME: path.join(root, "cache"), XDG_DATA_HOME: path.join(root, "data"), FAKE_CLAUDE_LOG: LOG,};fs.writeFileSync(env.DRAWGENT_CONFIG, `default_agent = "claude"\n\n[renderer]\nchrome = "${process.env.DRAWGENT_CHROME || "/usr/bin/google-chrome"}"\n\n[agents.claude]\ncli = "${FAKE}"\nbridge = ["@drawgent", "bridge", "claude", "--cli", "${FAKE}"]\n`);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 dg = (args, cwd) => spawnSync(BIN, args, { cwd, env, encoding: "utf8", timeout: 120000 });const entries = () => (fs.existsSync(LOG) ? fs.readFileSync(LOG, "utf8").trim().split("\n").filter(Boolean).map((l) => JSON.parse(l)) : []);// fetch keeps connections alive: after a restart on the same port, a pooled one to the old// drawgent fails once.const get = async (p, retry = true) => { try { const r = await fetch(API + p); return { status: r.status, body: await r.json().catch(() => ({})) }; } catch (e) { if (retry) return get(p, false); throw e; }};
// A chat message over the WebSocket; resolves with the chat events of the turn.const chatTurn = (text) => new Promise((res) => { const ws = new WebSocket(`ws://127.0.0.1:${PORT}/ws`); const events = []; ws.on("open", () => ws.send(JSON.stringify({ type: "chat", text }))); ws.on("message", (d) => { const m = JSON.parse(String(d)); if (m.type !== "chat") return; events.push(m.event); if (m.event.kind === "turn_end") (ws.close(), res(events)); }); setTimeout(() => (ws.close(), res(events)), 20000);});
const notRepo = path.join(root, "somewhere");fs.mkdirSync(notRepo, { recursive: true });try { // ---- background, outside any repository: canvas only. let r = dg(["up", "-d", "--port", String(PORT)], notRepo); ok("up -d returns once drawgent answers", r.status === 0 && r.stderr.includes("running in the background"), r.stderr.split("\n").slice(-7).join(" / ")); ok("outside a git repository it starts canvas only", r.stderr.includes("not in a git repository: starting canvas only") && r.stderr.includes("workspace: none (canvas only)")); r = dg(["status"], notRepo); ok("status: running, in the background, canvas only, agent", r.status === 0 && /in the background/.test(r.stdout) && r.stdout.includes("workspace: none (canvas only)") && /agent:\s+claude/.test(r.stdout), r.stdout.replace(/\n/g, " / ")); const h = await get("/api/health"); ok("the server says it has no workspace", h.body.workspace === false); const code = await get("/api/code/files"); ok("code endpoints refuse, saying why", code.status === 400 && JSON.stringify(code.body).includes("canvas only"), `${code.status} ${JSON.stringify(code.body).slice(0, 90)}`); for (let i = 0; i < 40 && !entries().some((e) => e.args); i++) await sleep(100); const args = entries().find((e) => e.args)?.args || []; ok("Claude Code runs without its built-in tools", args[args.indexOf("--tools") + 1] === "", args.slice(args.indexOf("--tools"), args.indexOf("--tools") + 2).join(" ")); ok("the MCP bridge leaves out the code tools", (args[args.indexOf("--mcp-config") + 1] || "").includes("--canvas-only")); ok("drawgent's state lives in its own directory, not here", fs.existsSync(path.join(env.XDG_DATA_HOME, "drawgent/canvas")) && !fs.existsSync(path.join(notRepo, ".drawgent"))); const events = await chatTurn("please WRITE a file"); const perm = entries().find((e) => e.permission)?.permission.response.response; ok("a file write is refused without asking anyone", perm?.behavior === "deny" && !events.some((e) => e.kind === "permission"), JSON.stringify(perm)); ok("and the chat says so", events.some((e) => e.kind === "status" && e.text.includes("refused (canvas only"))); const first = entries().find((e) => e.in?.type === "user")?.in.message.content[0].text || ""; ok("the agent is told there is no codebase", first.startsWith("**Canvas only:**")); r = dg(["up", "-d", "--port", String(PORT + 1)], notRepo); ok("a second up -d refuses: one is running", r.status !== 0 && r.stderr.includes("already running"), r.stderr.trim().split("\n").at(-1)); const pid = Number((dg(["status"], notRepo).stdout.match(/pid (\d+)/) || [])[1]); r = dg(["down"], notRepo); ok("down stops it", r.status === 0 && r.stderr.includes("drawgent stopped"), r.stderr.trim()); let gone = false; try { process.kill(pid, 0); } catch { gone = true; } ok("the process is gone and the boards were saved", gone && fs.existsSync(path.join(env.XDG_DATA_HOME, "drawgent/canvas/.drawgent"))); r = dg(["status"], notRepo); ok("status: not running (exit 1)", r.status === 1 && r.stderr.includes("not running")); const log = path.join(env.XDG_CACHE_HOME, "drawgent/drawgent.log"); ok("its output went to the log", fs.existsSync(log) && fs.readFileSync(log, "utf8").includes("started in the background"));
// The MCP bridge the agent gets, canvas only: no code tools. const tools = (flag) => { const input = [ { jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "t", version: "1" } } }, { jsonrpc: "2.0", method: "notifications/initialized" }, { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }, ].map((m) => JSON.stringify(m)).join("\n") + "\n"; const out = spawnSync(BIN, ["mcp", "--url", "http://127.0.0.1:1", ...flag], { input, encoding: "utf8", timeout: 5000 }).stdout; const res = out.trim().split("\n").map((l) => JSON.parse(l)).find((m) => m.id === 2); return (res?.result?.tools || []).map((t) => t.name); }; const all = tools([]), canvas = tools(["--canvas-only"]); ok("canvas-only MCP bridge has no code tools", all.includes("map_repo") && !canvas.some((t) => ["link_code", "check_links", "get_diagram_changes", "mark_progress", "map_repo"].includes(t)) && canvas.includes("add_graph"), `${all.length} → ${canvas.length} tools`);
// ---- in a repository: the repository is the workspace. const repo = path.join(root, "repo"); fs.mkdirSync(repo); execFileSync("git", ["-C", repo, "init", "-q"]); fs.writeFileSync(path.join(repo, "app.py"), "print('hi')\n"); r = dg(["up", "-d", "--port", String(PORT)], repo); ok("up -d in a repository uses it as the workspace", r.status === 0 && r.stderr.includes(`workspace: ${repo}`), r.stderr.split("\n").find((l) => l.includes("workspace"))); const files = await get("/api/code/files"); ok("with the code features", files.status === 200 && JSON.stringify(files.body).includes("app.py") && (await get("/api/health")).body.workspace === true); ok("down", dg(["down"], repo).status === 0);
// ---- --no-workspace in the foreground too. r = dg(["up", "-d", "--no-workspace", "--port", String(PORT)], repo); ok("--no-workspace makes it canvas only even in a repository", r.status === 0 && r.stderr.includes("workspace: none (canvas only)")); dg(["down"], repo);} catch (e) { console.error(e); checks.push(false);} finally { dg(["down"], root);}const failed = checks.filter((c) => !c).length;console.log(failed ? `DAEMON E2E FAILED (${failed})` : "DAEMON E2E OK");process.exit(failed ? 1 : 0);