Files
openclaw/src/gateway/server-runtime-state.test.ts
Sally O'Malley f3971bbd56 feat(mcp): support sandboxed MCP Apps (#69039)
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>
2026-07-11 22:04:38 -07:00

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,
}),
);
});
});