From 2745e5b3bd4942ea46d8140a895106012277918b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 17 Apr 2026 19:42:55 +0100 Subject: [PATCH] test: narrow canvas and context hotspots --- src/canvas-host/server.test.ts | 95 +++++++++++------------ src/context-engine/context-engine.test.ts | 16 ++-- 2 files changed, 54 insertions(+), 57 deletions(-) diff --git a/src/canvas-host/server.test.ts b/src/canvas-host/server.test.ts index f130396c466..74e3c6089df 100644 --- a/src/canvas-host/server.test.ts +++ b/src/canvas-host/server.test.ts @@ -1,13 +1,18 @@ import fs from "node:fs/promises"; import type { IncomingMessage } from "node:http"; -import { createRequire } from "node:module"; import os from "node:os"; import path from "node:path"; import type { Duplex } from "node:stream"; import { setTimeout as sleep } from "node:timers/promises"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { defaultRuntime } from "../runtime.js"; -import { A2UI_PATH, CANVAS_HOST_PATH, CANVAS_WS_PATH, injectCanvasLiveReload } from "./a2ui.js"; +import { + A2UI_PATH, + CANVAS_HOST_PATH, + CANVAS_WS_PATH, + handleA2uiHttpRequest, + injectCanvasLiveReload, +} from "./a2ui.js"; type MockWatcher = { on: (event: string, cb: (...args: unknown[]) => void) => MockWatcher; @@ -30,11 +35,6 @@ type CapturedResponse = { body: string; }; -function isLoopbackBindDenied(error: unknown) { - const code = (error as NodeJS.ErrnoException | undefined)?.code; - return code === "EPERM" || code === "EACCES"; -} - function createMockWatcherState() { const watchers: MockWatcher[] = []; const createWatcher = () => { @@ -95,6 +95,35 @@ async function captureHandlerResponse( return response; } +async function captureA2uiResponse(url: string, method = "GET"): Promise { + const response: CapturedResponse = { + handled: false, + status: 200, + headers: {}, + body: "", + }; + const res = { + statusCode: 200, + setHeader(name: string, value: number | string | readonly string[]) { + const headerValue: number | string | string[] = + typeof value === "object" ? [...value] : value; + response.headers[name.toLowerCase()] = headerValue; + return this; + }, + end(chunk?: string | Buffer) { + response.status = this.statusCode; + response.body = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : (chunk ?? ""); + return this; + }, + }; + response.handled = await handleA2uiHttpRequest( + { method, url } as IncomingMessage, + res as import("node:http").ServerResponse, + ); + response.status = res.statusCode; + return response; +} + describe("canvas host", () => { const quietRuntime = { ...defaultRuntime, @@ -102,7 +131,6 @@ describe("canvas host", () => { }; let createCanvasHostHandler: typeof import("./server.js").createCanvasHostHandler; let startCanvasHost: typeof import("./server.js").startCanvasHost; - let realFetch: typeof import("undici").fetch; let WebSocketServerClass: typeof import("ws").WebSocketServer; let watcherState: ReturnType; let fixtureRoot = ""; @@ -114,29 +142,10 @@ describe("canvas host", () => { return dir; }; - const startFixtureCanvasHost = async ( - rootDir: string, - overrides: Partial[0]> = {}, - ) => - await startCanvasHost({ - runtime: quietRuntime, - rootDir, - port: 0, - listenHost: "127.0.0.1", - allowInTests: true, - watchFactory: watcherState.watchFactory as unknown as Parameters< - typeof startCanvasHost - >[0]["watchFactory"], - webSocketServerClass: WebSocketServerClass, - ...overrides, - }); - beforeAll(async () => { vi.doUnmock("undici"); vi.resetModules(); - const require = createRequire(import.meta.url); ({ createCanvasHostHandler, startCanvasHost } = await import("./server.js")); - ({ fetch: realFetch } = require("undici") as typeof import("undici")); const wsModule = await vi.importActual("ws"); WebSocketServerClass = wsModule.WebSocketServer; fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-canvas-fixtures-")); @@ -363,14 +372,12 @@ describe("canvas host", () => { ); it("serves A2UI scaffold and blocks traversal/symlink escapes", async () => { - const dir = await createCaseDir(); const a2uiRoot = path.resolve(process.cwd(), "src/canvas-host/a2ui"); const bundlePath = path.join(a2uiRoot, "a2ui.bundle.js"); const linkName = `test-link-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`; const linkPath = path.join(a2uiRoot, linkName); let createdBundle = false; let createdLink = false; - let server: Awaited> | undefined; try { await fs.stat(bundlePath); @@ -383,37 +390,23 @@ describe("canvas host", () => { createdLink = true; try { - try { - server = await startFixtureCanvasHost(dir); - } catch (error) { - if (isLoopbackBindDenied(error)) { - return; - } - throw error; - } - - const res = await realFetch(`http://127.0.0.1:${server.port}/__openclaw__/a2ui/`); - const html = await res.text(); + const res = await captureA2uiResponse(`${A2UI_PATH}/`); + const html = res.body; expect(res.status).toBe(200); expect(html).toContain("openclaw-a2ui-host"); expect(html).toContain("openclawCanvasA2UIAction"); - const bundleRes = await realFetch( - `http://127.0.0.1:${server.port}/__openclaw__/a2ui/a2ui.bundle.js`, - ); - const js = await bundleRes.text(); + const bundleRes = await captureA2uiResponse(`${A2UI_PATH}/a2ui.bundle.js`); + const js = bundleRes.body; expect(bundleRes.status).toBe(200); expect(js).toContain("openclawA2UI"); - const traversalRes = await realFetch( - `http://127.0.0.1:${server.port}${A2UI_PATH}/%2e%2e%2fpackage.json`, - ); + const traversalRes = await captureA2uiResponse(`${A2UI_PATH}/%2e%2e%2fpackage.json`); expect(traversalRes.status).toBe(404); - expect(await traversalRes.text()).toBe("not found"); - const symlinkRes = await realFetch(`http://127.0.0.1:${server.port}${A2UI_PATH}/${linkName}`); + expect(traversalRes.body).toBe("not found"); + const symlinkRes = await captureA2uiResponse(`${A2UI_PATH}/${linkName}`); expect(symlinkRes.status).toBe(404); - expect(await symlinkRes.text()).toBe("not found"); + expect(symlinkRes.body).toBe("not found"); } finally { - await server?.close(); if (createdLink) { await fs.rm(linkPath, { force: true }); } diff --git a/src/context-engine/context-engine.test.ts b/src/context-engine/context-engine.test.ts index 3f80ed88f21..239ccd7b334 100644 --- a/src/context-engine/context-engine.test.ts +++ b/src/context-engine/context-engine.test.ts @@ -1009,12 +1009,16 @@ describe("Bundle chunk isolation (#40096)", () => { it("shares registrations and keeps concurrent chunk registration visible", async () => { const ts = Date.now().toString(36); const registryUrl = new URL("./registry.ts", import.meta.url).href; - const chunks = await Promise.all( - Array.from( - { length: 2 }, - (_, i) => import(/* @vite-ignore */ `${registryUrl}?chunk=${ts}-${i}`), - ), - ); + const dynamicChunk = await import(/* @vite-ignore */ `${registryUrl}?chunk=${ts}-dynamic`); + const chunks = [ + { + registerContextEngine, + getContextEngineFactory, + listContextEngineIds, + resolveContextEngine, + }, + dynamicChunk, + ]; const engineId = `cross-chunk-${ts}`; const factory = () => ({