































@@ -1,7 +1,6 @@
11import fs from "node:fs/promises";
2-import { createServer, type IncomingMessage } from "node:http";
2+import type { IncomingMessage } from "node:http";
33import { createRequire } from "node:module";
4-import type { AddressInfo } from "node:net";
54import os from "node:os";
65import path from "node:path";
76import type { Duplex } from "node:stream";
@@ -24,6 +23,13 @@ type TrackingWebSocket = {
2423send: (message: string) => void;
2524};
262526+type CapturedResponse = {
27+handled: boolean;
28+status: number;
29+headers: Record<string, number | string | string[]>;
30+body: string;
31+};
32+2733function isLoopbackBindDenied(error: unknown) {
2834const code = (error as NodeJS.ErrnoException | undefined)?.code;
2935return code === "EPERM" || code === "EACCES";
@@ -56,6 +62,37 @@ function createMockWatcherState() {
5662};
5763}
586465+async function captureHandlerResponse(
66+handler: Pick<import("./server.js").CanvasHostHandler, "handleHttpRequest">,
67+url: string,
68+method = "GET",
69+): Promise<CapturedResponse> {
70+const response: CapturedResponse = {
71+handled: false,
72+status: 200,
73+headers: {},
74+body: "",
75+};
76+const res = {
77+statusCode: 200,
78+setHeader(name: string, value: number | string | readonly string[]) {
79+response.headers[name.toLowerCase()] = Array.isArray(value) ? [...value] : value;
80+return this;
81+},
82+end(chunk?: string | Buffer) {
83+response.status = this.statusCode;
84+response.body = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : (chunk ?? "");
85+return this;
86+},
87+};
88+response.handled = await handler.handleHttpRequest(
89+{ method, url } as IncomingMessage,
90+res as import("node:http").ServerResponse,
91+);
92+response.status = res.statusCode;
93+return response;
94+}
95+5996describe("canvas host", () => {
6097const quietRuntime = {
6198 ...defaultRuntime,
@@ -92,12 +129,6 @@ describe("canvas host", () => {
92129 ...overrides,
93130});
9413195-const fetchCanvasHtml = async (port: number) => {
96-const res = await realFetch(`http://127.0.0.1:${port}${CANVAS_HOST_PATH}/`);
97-const html = await res.text();
98-return { res, html };
99-};
100-101132beforeAll(async () => {
102133vi.doUnmock("undici");
103134vi.resetModules();
@@ -128,52 +159,55 @@ describe("canvas host", () => {
128159129160it("creates a default index.html when missing", async () => {
130161const dir = await createCaseDir();
131-let server: Awaited<ReturnType<typeof startFixtureCanvasHost>>;
132-try {
133-server = await startFixtureCanvasHost(dir);
134-} catch (error) {
135-if (isLoopbackBindDenied(error)) {
136-return;
137-}
138-throw error;
139-}
162+const handler = await createCanvasHostHandler({
163+runtime: quietRuntime,
164+rootDir: dir,
165+basePath: CANVAS_HOST_PATH,
166+allowInTests: true,
167+watchFactory: watcherState.watchFactory as unknown as Parameters<
168+typeof createCanvasHostHandler
169+>[0]["watchFactory"],
170+webSocketServerClass: WebSocketServerClass,
171+});
140172141173try {
142-const { res, html } = await fetchCanvasHtml(server.port);
143-expect(res.status).toBe(200);
144-expect(html).toContain("Interactive test page");
145-expect(html).toContain("openclawSendUserAction");
146-expect(html).toContain(CANVAS_WS_PATH);
147-expect(html).toContain('document.createElement("span")');
148-expect(html).not.toContain("statusEl.innerHTML");
174+const response = await captureHandlerResponse(handler, `${CANVAS_HOST_PATH}/`);
175+expect(response.status).toBe(200);
176+expect(response.body).toContain("Interactive test page");
177+expect(response.body).toContain("openclawSendUserAction");
178+expect(response.body).toContain(CANVAS_WS_PATH);
179+expect(response.body).toContain('document.createElement("span")');
180+expect(response.body).not.toContain("statusEl.innerHTML");
149181} finally {
150-await server.close();
182+await handler.close();
151183}
152184});
153185154186it("skips live reload injection when disabled", async () => {
155187const dir = await createCaseDir();
156188await fs.writeFile(path.join(dir, "index.html"), "<html><body>no-reload</body></html>", "utf8");
157-let server: Awaited<ReturnType<typeof startFixtureCanvasHost>>;
158-try {
159-server = await startFixtureCanvasHost(dir, { liveReload: false });
160-} catch (error) {
161-if (isLoopbackBindDenied(error)) {
162-return;
163-}
164-throw error;
165-}
189+const handler = await createCanvasHostHandler({
190+runtime: quietRuntime,
191+rootDir: dir,
192+basePath: CANVAS_HOST_PATH,
193+allowInTests: true,
194+liveReload: false,
195+watchFactory: watcherState.watchFactory as unknown as Parameters<
196+typeof createCanvasHostHandler
197+>[0]["watchFactory"],
198+webSocketServerClass: WebSocketServerClass,
199+});
166200167201try {
168-const { res, html } = await fetchCanvasHtml(server.port);
169-expect(res.status).toBe(200);
170-expect(html).toContain("no-reload");
171-expect(html).not.toContain(CANVAS_WS_PATH);
202+const response = await captureHandlerResponse(handler, `${CANVAS_HOST_PATH}/`);
203+expect(response.status).toBe(200);
204+expect(response.body).toContain("no-reload");
205+expect(response.body).not.toContain(CANVAS_WS_PATH);
172206173-const wsRes = await realFetch(`http://127.0.0.1:${server.port}${CANVAS_WS_PATH}`);
174-expect(wsRes.status).toBe(404);
207+const wsResponse = await captureHandlerResponse(handler, CANVAS_WS_PATH);
208+expect(wsResponse.status).toBe(404);
175209} finally {
176-await server.close();
210+await handler.close();
177211}
178212});
179213@@ -192,77 +226,35 @@ describe("canvas host", () => {
192226webSocketServerClass: WebSocketServerClass,
193227});
194228195-const server = createServer((req, res) => {
196-void (async () => {
197-if (await handler.handleHttpRequest(req, res)) {
198-return;
199-}
200-res.statusCode = 404;
201-res.setHeader("Content-Type", "text/plain; charset=utf-8");
202-res.end("Not Found");
203-})();
204-});
205-server.on("upgrade", (req, socket, head) => {
206-if (handler.handleUpgrade(req, socket, head)) {
207-return;
208-}
209-socket.destroy();
210-});
211-try {
212-await new Promise<void>((resolve, reject) => {
213-const onError = (error: Error) => {
214-server.off("listening", onListening);
215-reject(error);
216-};
217-const onListening = () => {
218-server.off("error", onError);
219-resolve();
220-};
221-server.once("error", onError);
222-server.once("listening", onListening);
223-server.listen(0, "127.0.0.1");
224-});
225-} catch (error) {
226-await handler.close();
227-if (isLoopbackBindDenied(error)) {
228-return;
229-}
230-throw error;
231-}
232-const port = (server.address() as AddressInfo).port;
229+const originalClose = handler.close;
230+const closeSpy = vi.fn(async () => originalClose());
233231234232try {
235-const res = await realFetch(`http://127.0.0.1:${port}${CANVAS_HOST_PATH}/`);
236-const html = await res.text();
237-expect(res.status).toBe(200);
238-expect(html).toContain("v1");
239-expect(html).toContain(CANVAS_WS_PATH);
233+const response = await captureHandlerResponse(handler, `${CANVAS_HOST_PATH}/`);
234+expect(response.status).toBe(200);
235+expect(response.body).toContain("v1");
236+expect(response.body).toContain(CANVAS_WS_PATH);
240237241-const miss = await realFetch(`http://127.0.0.1:${port}/`);
242-expect(miss.status).toBe(404);
243-} finally {
244-await new Promise<void>((resolve, reject) =>
245-server.close((err) => (err ? reject(err) : resolve())),
246-);
247-}
248-const originalClose = handler.close;
249-const closeSpy = vi.fn(async () => originalClose());
250-handler.close = closeSpy;
238+const miss = await captureHandlerResponse(handler, "/");
239+expect(miss.handled).toBe(false);
251240252-const hosted = await startCanvasHost({
253-runtime: quietRuntime,
254- handler,
255-ownsHandler: false,
256-port: 0,
257-listenHost: "127.0.0.1",
258-allowInTests: true,
259-});
241+handler.close = closeSpy;
242+const hosted = await startCanvasHost({
243+runtime: quietRuntime,
244+ handler,
245+ownsHandler: false,
246+port: 0,
247+listenHost: "127.0.0.1",
248+allowInTests: true,
249+});
260250261-try {
262-expect(hosted.port).toBeGreaterThan(0);
251+try {
252+expect(hosted.port).toBeGreaterThan(0);
253+} finally {
254+await hosted.close();
255+expect(closeSpy).not.toHaveBeenCalled();
256+}
263257} finally {
264-await hosted.close();
265-expect(closeSpy).not.toHaveBeenCalled();
266258await originalClose();
267259}
268260});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。