Files
openclaw/src/gateway/control-ui.auto-root.http.test.ts
Radek Sienkiewicz 4f42c03a49 gateway: fix global Control UI 404s for symlinked wrappers and bundled package roots (#40385)
Merged via squash.

Prepared head SHA: 567b3ed684
Co-authored-by: velvet-shark <126378+velvet-shark@users.noreply.github.com>
Co-authored-by: velvet-shark <126378+velvet-shark@users.noreply.github.com>
Reviewed-by: @velvet-shark
2026-03-09 01:50:42 +01:00

102 lines
3.9 KiB
TypeScript

import fs from "node:fs/promises";
import type { IncomingMessage } from "node:http";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
const { resolveControlUiRootSyncMock, isPackageProvenControlUiRootSyncMock } = vi.hoisted(() => ({
resolveControlUiRootSyncMock: vi.fn(),
isPackageProvenControlUiRootSyncMock: vi.fn().mockReturnValue(true),
}));
vi.mock("../infra/control-ui-assets.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../infra/control-ui-assets.js")>();
return {
...actual,
resolveControlUiRootSync: resolveControlUiRootSyncMock,
isPackageProvenControlUiRootSync: isPackageProvenControlUiRootSyncMock,
};
});
const { handleControlUiHttpRequest } = await import("./control-ui.js");
const { makeMockHttpResponse } = await import("./test-http-response.js");
async function withControlUiRoot<T>(fn: (tmp: string) => Promise<T>) {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-ui-auto-root-"));
try {
await fs.writeFile(path.join(tmp, "index.html"), "<html>fallback</html>\n");
return await fn(tmp);
} finally {
await fs.rm(tmp, { recursive: true, force: true });
}
}
afterEach(() => {
resolveControlUiRootSyncMock.mockReset();
isPackageProvenControlUiRootSyncMock.mockReset();
isPackageProvenControlUiRootSyncMock.mockReturnValue(true);
});
describe("handleControlUiHttpRequest auto-detected root", () => {
it("serves hardlinked asset files for bundled auto-detected roots", async () => {
await withControlUiRoot(async (tmp) => {
const assetsDir = path.join(tmp, "assets");
await fs.mkdir(assetsDir, { recursive: true });
await fs.writeFile(path.join(assetsDir, "app.js"), "console.log('hi');");
await fs.link(path.join(assetsDir, "app.js"), path.join(assetsDir, "app.hl.js"));
resolveControlUiRootSyncMock.mockReturnValue(tmp);
const { res, end } = makeMockHttpResponse();
const handled = handleControlUiHttpRequest(
{ url: "/assets/app.hl.js", method: "GET" } as IncomingMessage,
res,
);
expect(handled).toBe(true);
expect(res.statusCode).toBe(200);
expect(String(end.mock.calls[0]?.[0] ?? "")).toBe("console.log('hi');");
});
});
it("serves hardlinked SPA fallback index.html for bundled auto-detected roots", async () => {
await withControlUiRoot(async (tmp) => {
const sourceIndex = path.join(tmp, "index.source.html");
const indexPath = path.join(tmp, "index.html");
await fs.writeFile(sourceIndex, "<html>fallback-hardlink</html>\n");
await fs.rm(indexPath);
await fs.link(sourceIndex, indexPath);
resolveControlUiRootSyncMock.mockReturnValue(tmp);
const { res, end } = makeMockHttpResponse();
const handled = handleControlUiHttpRequest(
{ url: "/dashboard", method: "GET" } as IncomingMessage,
res,
);
expect(handled).toBe(true);
expect(res.statusCode).toBe(200);
expect(String(end.mock.calls[0]?.[0] ?? "")).toBe("<html>fallback-hardlink</html>\n");
});
});
it("rejects hardlinked assets for non-package-proven auto-detected roots", async () => {
isPackageProvenControlUiRootSyncMock.mockReturnValue(false);
await withControlUiRoot(async (tmp) => {
const assetsDir = path.join(tmp, "assets");
await fs.mkdir(assetsDir, { recursive: true });
await fs.writeFile(path.join(assetsDir, "app.js"), "console.log('hi');");
await fs.link(path.join(assetsDir, "app.js"), path.join(assetsDir, "app.hl.js"));
resolveControlUiRootSyncMock.mockReturnValue(tmp);
const { res } = makeMockHttpResponse();
const handled = handleControlUiHttpRequest(
{ url: "/assets/app.hl.js", method: "GET" } as IncomingMessage,
res,
);
expect(handled).toBe(true);
expect(res.statusCode).toBe(404);
});
});
});