mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-20 02:41:41 +00:00
Implement stable opt-in MCP Apps negotiation, caller-specific tool visibility, bounded ephemeral UI resources, a dedicated-origin double iframe host, restrictive CSP, configuration, docs, and regression tests. Co-authored-by: Peter Steinberger <steipete@gmail.com> Co-authored-by: Sally O'Malley <11166065+sallyom@users.noreply.github.com>
153 lines
5.7 KiB
TypeScript
153 lines
5.7 KiB
TypeScript
/**
|
|
* Gateway runtime state construction tests.
|
|
*/
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { createEmptyPluginRegistry } from "../plugins/registry.js";
|
|
import {
|
|
getActivePluginChannelRegistry,
|
|
getActivePluginSessionExtensionRegistry,
|
|
pinActivePluginHttpRouteRegistry,
|
|
pinActivePluginChannelRegistry,
|
|
pinActivePluginSessionExtensionRegistry,
|
|
releasePinnedPluginChannelRegistry,
|
|
releasePinnedPluginHttpRouteRegistry,
|
|
releasePinnedPluginSessionExtensionRegistry,
|
|
resetPluginRuntimeStateForTest,
|
|
resolveActivePluginHttpRouteRegistry,
|
|
setActivePluginRegistry,
|
|
} from "../plugins/runtime.js";
|
|
import { createGatewayRuntimeStateForTest } from "./test-helpers.server-runtime-state.js";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
listenGatewayHttpServer: vi.fn(
|
|
async (_params: { bindHost: string; retryEaddrinuse?: boolean }) => {},
|
|
),
|
|
resolveGatewayListenHosts: vi.fn(async (_bindHost: string) => ["127.0.0.1"]),
|
|
}));
|
|
|
|
vi.mock("./server/http-listen.js", () => ({
|
|
listenGatewayHttpServer: mocks.listenGatewayHttpServer,
|
|
}));
|
|
|
|
vi.mock("./net.js", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("./net.js")>();
|
|
return { ...actual, resolveGatewayListenHosts: mocks.resolveGatewayListenHosts };
|
|
});
|
|
|
|
function createRegistryWithRoute(path: string) {
|
|
const registry = createEmptyPluginRegistry();
|
|
registry.httpRoutes.push({
|
|
path,
|
|
auth: "plugin",
|
|
match: "exact",
|
|
handler: () => true,
|
|
pluginId: "demo",
|
|
source: "test",
|
|
});
|
|
return registry;
|
|
}
|
|
|
|
describe("createGatewayRuntimeState", () => {
|
|
beforeEach(() => {
|
|
mocks.listenGatewayHttpServer.mockReset();
|
|
mocks.listenGatewayHttpServer.mockResolvedValue(undefined);
|
|
mocks.resolveGatewayListenHosts.mockReset();
|
|
mocks.resolveGatewayListenHosts.mockResolvedValue(["127.0.0.1"]);
|
|
});
|
|
|
|
afterEach(() => {
|
|
releasePinnedPluginHttpRouteRegistry();
|
|
releasePinnedPluginChannelRegistry();
|
|
releasePinnedPluginSessionExtensionRegistry();
|
|
resetPluginRuntimeStateForTest();
|
|
});
|
|
|
|
it("releases post-bootstrap repinned plugin registries on cleanup", async () => {
|
|
const startupRegistry = createRegistryWithRoute("/startup");
|
|
const loadedRegistry = createRegistryWithRoute("/loaded");
|
|
const fallbackRegistry = createRegistryWithRoute("/fallback");
|
|
|
|
setActivePluginRegistry(startupRegistry);
|
|
const runtimeState = await createGatewayRuntimeStateForTest(startupRegistry);
|
|
|
|
pinActivePluginHttpRouteRegistry(loadedRegistry);
|
|
pinActivePluginSessionExtensionRegistry(loadedRegistry);
|
|
pinActivePluginChannelRegistry(loadedRegistry);
|
|
expect(resolveActivePluginHttpRouteRegistry(fallbackRegistry)).toBe(loadedRegistry);
|
|
expect(getActivePluginSessionExtensionRegistry()).toBe(loadedRegistry);
|
|
expect(getActivePluginChannelRegistry()).toBe(loadedRegistry);
|
|
|
|
runtimeState.releasePluginRouteRegistry();
|
|
|
|
expect(resolveActivePluginHttpRouteRegistry(fallbackRegistry)).toBe(startupRegistry);
|
|
expect(getActivePluginSessionExtensionRegistry()).toBe(startupRegistry);
|
|
expect(getActivePluginChannelRegistry()).toBe(startupRegistry);
|
|
});
|
|
|
|
it("fails startup when the required IPv4 loopback alias cannot bind", async () => {
|
|
const warn = vi.fn();
|
|
mocks.resolveGatewayListenHosts.mockResolvedValue(["100.64.0.1", "127.0.0.1"]);
|
|
mocks.listenGatewayHttpServer.mockImplementation(async ({ bindHost }) => {
|
|
if (bindHost === "127.0.0.1") {
|
|
throw new Error("loopback occupied");
|
|
}
|
|
});
|
|
const runtimeState = await createGatewayRuntimeStateForTest(undefined, {
|
|
bindHost: "100.64.0.1",
|
|
log: { info: () => {}, warn },
|
|
});
|
|
|
|
await expect(runtimeState.startListening()).rejects.toThrow("loopback occupied");
|
|
await expect(runtimeState.startListening()).rejects.toThrow("loopback occupied");
|
|
expect(mocks.listenGatewayHttpServer).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.objectContaining({ bindHost: "127.0.0.1", retryEaddrinuse: false }),
|
|
);
|
|
expect(mocks.listenGatewayHttpServer).toHaveBeenCalledTimes(1);
|
|
expect(runtimeState.httpBindHosts).toEqual([]);
|
|
expect(warn).not.toHaveBeenCalledWith(expect.stringContaining("failed to bind loopback alias"));
|
|
});
|
|
|
|
it("keeps the optional IPv6 loopback alias non-fatal", async () => {
|
|
const warn = vi.fn();
|
|
mocks.resolveGatewayListenHosts.mockResolvedValue(["127.0.0.1", "::1"]);
|
|
mocks.listenGatewayHttpServer.mockImplementation(async ({ bindHost }) => {
|
|
if (bindHost === "::1") {
|
|
throw new Error("IPv6 unavailable");
|
|
}
|
|
});
|
|
const runtimeState = await createGatewayRuntimeStateForTest(undefined, {
|
|
log: { info: () => {}, warn },
|
|
});
|
|
|
|
await expect(runtimeState.startListening()).resolves.toBeUndefined();
|
|
expect(runtimeState.httpBindHosts).toEqual(["127.0.0.1"]);
|
|
expect(warn).toHaveBeenCalledWith(expect.stringContaining("failed to bind loopback alias ::1"));
|
|
});
|
|
|
|
it("starts MCP Apps on a dedicated adjacent-port origin", async () => {
|
|
const runtimeState = await createGatewayRuntimeStateForTest(undefined, {
|
|
cfg: { mcp: { apps: { enabled: true } } },
|
|
port: 18789,
|
|
});
|
|
|
|
expect(runtimeState.getMcpAppSandboxPort()).toBeUndefined();
|
|
await runtimeState.startListening();
|
|
|
|
expect(runtimeState.getMcpAppSandboxPort()).toBe(18790);
|
|
expect(runtimeState.httpServers).toHaveLength(2);
|
|
expect(mocks.listenGatewayHttpServer).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.objectContaining({ bindHost: "127.0.0.1", port: 18789 }),
|
|
);
|
|
expect(mocks.listenGatewayHttpServer).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.objectContaining({
|
|
bindHost: "127.0.0.1",
|
|
port: 18790,
|
|
retryEaddrinuse: false,
|
|
}),
|
|
);
|
|
});
|
|
});
|