import fs from "node:fs/promises"; import { createServer } from "node:http"; import type { AddressInfo } from "node:net"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { WebSocket } from "ws"; import { rawDataToString } from "../infra/ws.js"; import { defaultRuntime } from "../runtime.js"; import { CANVAS_HOST_PATH, CANVAS_WS_PATH, injectCanvasLiveReload, } from "./a2ui.js"; import { createCanvasHostHandler, startCanvasHost } from "./server.js"; describe("canvas host", () => { it("injects live reload script", () => { const out = injectCanvasLiveReload("Hello"); expect(out).toContain(CANVAS_WS_PATH); expect(out).toContain("location.reload"); expect(out).toContain("clawdisCanvasA2UIAction"); expect(out).toContain("clawdisSendUserAction"); }); it("creates a default index.html when missing", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-canvas-")); const server = await startCanvasHost({ runtime: defaultRuntime, rootDir: dir, port: 0, listenHost: "127.0.0.1", allowInTests: true, }); try { const res = await fetch( `http://127.0.0.1:${server.port}${CANVAS_HOST_PATH}/`, ); const html = await res.text(); expect(res.status).toBe(200); expect(html).toContain("Interactive test page"); expect(html).toContain("clawdisSendUserAction"); expect(html).toContain(CANVAS_WS_PATH); } finally { await server.close(); await fs.rm(dir, { recursive: true, force: true }); } }); it("serves canvas content from the mounted base path", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-canvas-")); await fs.writeFile( path.join(dir, "index.html"), "v1", "utf8", ); const handler = await createCanvasHostHandler({ runtime: defaultRuntime, rootDir: dir, basePath: CANVAS_HOST_PATH, allowInTests: true, }); const server = createServer((req, res) => { void (async () => { if (await handler.handleHttpRequest(req, res)) return; res.statusCode = 404; res.setHeader("Content-Type", "text/plain; charset=utf-8"); res.end("Not Found"); })(); }); server.on("upgrade", (req, socket, head) => { if (handler.handleUpgrade(req, socket, head)) return; socket.destroy(); }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve), ); const port = (server.address() as AddressInfo).port; try { const res = await fetch(`http://127.0.0.1:${port}${CANVAS_HOST_PATH}/`); const html = await res.text(); expect(res.status).toBe(200); expect(html).toContain("v1"); expect(html).toContain(CANVAS_WS_PATH); const miss = await fetch(`http://127.0.0.1:${port}/`); expect(miss.status).toBe(404); } finally { await handler.close(); await new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve())), ); await fs.rm(dir, { recursive: true, force: true }); } }); it("serves HTML with injection and broadcasts reload on file changes", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-canvas-")); const index = path.join(dir, "index.html"); await fs.writeFile(index, "v1", "utf8"); const server = await startCanvasHost({ runtime: defaultRuntime, rootDir: dir, port: 0, listenHost: "127.0.0.1", allowInTests: true, }); try { const res = await fetch( `http://127.0.0.1:${server.port}${CANVAS_HOST_PATH}/`, ); const html = await res.text(); expect(res.status).toBe(200); expect(html).toContain("v1"); expect(html).toContain(CANVAS_WS_PATH); const ws = new WebSocket( `ws://127.0.0.1:${server.port}${CANVAS_WS_PATH}`, ); await new Promise((resolve, reject) => { const timer = setTimeout( () => reject(new Error("ws open timeout")), 2000, ); ws.on("open", () => { clearTimeout(timer); resolve(); }); ws.on("error", (err) => { clearTimeout(timer); reject(err); }); }); const msg = new Promise((resolve, reject) => { const timer = setTimeout( () => reject(new Error("reload timeout")), 4000, ); ws.on("message", (data) => { clearTimeout(timer); resolve(rawDataToString(data)); }); }); await fs.writeFile(index, "v2", "utf8"); expect(await msg).toBe("reload"); ws.close(); } finally { await server.close(); await fs.rm(dir, { recursive: true, force: true }); } }); it("serves the gateway-hosted A2UI scaffold", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-canvas-")); const server = await startCanvasHost({ runtime: defaultRuntime, rootDir: dir, port: 0, listenHost: "127.0.0.1", allowInTests: true, }); try { const res = await fetch( `http://127.0.0.1:${server.port}/__clawdis__/a2ui/`, ); const html = await res.text(); expect(res.status).toBe(200); expect(html).toContain("clawdis-a2ui-host"); expect(html).toContain("clawdisCanvasA2UIAction"); const bundleRes = await fetch( `http://127.0.0.1:${server.port}/__clawdis__/a2ui/a2ui.bundle.js`, ); const js = await bundleRes.text(); expect(bundleRes.status).toBe(200); expect(js).toContain("clawdisA2UI"); } finally { await server.close(); await fs.rm(dir, { recursive: true, force: true }); } }); });