Files
openclaw/extensions/mxc/test/plugin.test.ts
Paul Campbell 008f04a656 feat(mxc): add Windows MXC sandbox backend (#97086)
* feat(mxc): add Windows MXC sandbox backend

Add the official MXC sandbox plugin package with Windows ProcessContainer execution, plugin-owned MXC SDK dependency packaging, host-backed filesystem bridge support, and configured MXC policy file loading via mxcPolicyPaths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(mxc): preserve Windows binary override paths

* fix: remove stray sandbox barrel export

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 9ea19539-b8ca-44fb-93bd-b8496e3deb2c

* fix(mxc): address sandbox review feedback

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(mxc): satisfy test type checks

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(mxc): clarify protected skill enforcement

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* test(mxc): align fail-closed expectations

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(mxc): satisfy extension lint

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(plugin-sdk): narrow fs-safe remove surface

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(mxc): repair rebased CI failures

* fix(scripts): declare shrinkwrap override normalizer

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Gio Della-Libera <235387111+giodl73-repo@users.noreply.github.com>
Co-authored-by: Dallin Romney <dallinromney@gmail.com>
2026-07-12 23:07:25 -07:00

200 lines
6.2 KiB
TypeScript

import type {
OpenClawPluginApi,
OpenClawPluginService,
OpenClawPluginServiceContext,
} from "openclaw/plugin-sdk/plugin-entry";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
const {
assertMxcReadinessMock,
warnMxcHostPrepIfNeededMock,
createMxcSandboxBackendFactoryMock,
factoryMock,
mxcSandboxBackendManagerMock,
registerSandboxBackendMock,
resolveMxcBinaryPathMock,
unregisterMock,
} = vi.hoisted(() => {
const factory = { id: "factory" };
const unregister = vi.fn();
return {
assertMxcReadinessMock: vi.fn(),
warnMxcHostPrepIfNeededMock: vi.fn(),
createMxcSandboxBackendFactoryMock: vi.fn(() => factory),
factoryMock: factory,
mxcSandboxBackendManagerMock: { id: "manager" },
registerSandboxBackendMock: vi.fn(() => unregister),
resolveMxcBinaryPathMock: vi.fn(() => "mxc-test-binary"),
unregisterMock: unregister,
};
});
vi.mock("openclaw/plugin-sdk/sandbox", () => ({
registerSandboxBackend: registerSandboxBackendMock,
}));
vi.mock("../src/binary-resolver.js", () => ({
resolveMxcBinaryPath: resolveMxcBinaryPathMock,
}));
vi.mock("../src/mxc-backend-factory.js", () => ({
createMxcSandboxBackendFactory: createMxcSandboxBackendFactoryMock,
}));
vi.mock("../src/mxc-backend.js", () => ({
mxcSandboxBackendManager: mxcSandboxBackendManagerMock,
}));
vi.mock("../src/readiness.js", () => ({
assertMxcReadiness: assertMxcReadinessMock,
warnMxcHostPrepIfNeeded: warnMxcHostPrepIfNeededMock,
}));
import { registerMxcPlugin } from "../src/plugin.js";
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
type MxcPluginApiForTest = Pick<
OpenClawPluginApi,
"pluginConfig" | "registerService" | "registrationMode"
>;
const nonFullRegistrationModes = [
"discovery",
"tool-discovery",
"setup-only",
"setup-runtime",
"cli-metadata",
] as const satisfies readonly OpenClawPluginApi["registrationMode"][];
function setProcessPlatformForTest(platform: NodeJS.Platform): void {
Object.defineProperty(process, "platform", {
configurable: true,
enumerable: true,
value: platform,
});
}
function restoreProcessPlatformForTest(): void {
if (originalPlatform) {
Object.defineProperty(process, "platform", originalPlatform);
}
}
function createApi(
pluginConfig: Record<string, unknown> | undefined = {},
registrationMode: OpenClawPluginApi["registrationMode"] = "full",
): {
api: OpenClawPluginApi;
registerService: ReturnType<typeof vi.fn>;
services: OpenClawPluginService[];
} {
const services: OpenClawPluginService[] = [];
const registerService = vi.fn((service: OpenClawPluginService): void => {
services.push(service);
});
const api = {
pluginConfig,
registrationMode,
registerService,
} satisfies MxcPluginApiForTest;
return {
api: api as unknown as OpenClawPluginApi,
registerService,
services,
};
}
describe("registerMxcPlugin", () => {
let warnSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
assertMxcReadinessMock.mockClear();
warnMxcHostPrepIfNeededMock.mockClear();
createMxcSandboxBackendFactoryMock.mockClear();
registerSandboxBackendMock.mockClear();
resolveMxcBinaryPathMock.mockReset();
resolveMxcBinaryPathMock.mockReturnValue("mxc-test-binary");
unregisterMock.mockClear();
setProcessPlatformForTest("win32");
warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
});
afterEach(() => {
warnSpy.mockRestore();
restoreProcessPlatformForTest();
});
test("warns and stays dormant on non-Windows platforms", () => {
setProcessPlatformForTest("darwin");
const { api, registerService } = createApi();
registerMxcPlugin(api);
expect(warnSpy).toHaveBeenCalledWith(
"[mxc] Sandbox backend is Windows-only and not available on darwin. Plugin will be dormant.",
);
expect(resolveMxcBinaryPathMock).not.toHaveBeenCalled();
expect(assertMxcReadinessMock).not.toHaveBeenCalled();
expect(registerSandboxBackendMock).not.toHaveBeenCalled();
expect(registerService).not.toHaveBeenCalled();
});
test.each(nonFullRegistrationModes)(
"does not register runtime hooks during %s registration",
(registrationMode) => {
const { api, registerService } = createApi({ timeoutSeconds: 60 }, registrationMode);
registerMxcPlugin(api);
expect(warnSpy).not.toHaveBeenCalled();
expect(resolveMxcBinaryPathMock).not.toHaveBeenCalled();
expect(assertMxcReadinessMock).not.toHaveBeenCalled();
expect(warnMxcHostPrepIfNeededMock).not.toHaveBeenCalled();
expect(createMxcSandboxBackendFactoryMock).not.toHaveBeenCalled();
expect(registerSandboxBackendMock).not.toHaveBeenCalled();
expect(registerService).not.toHaveBeenCalled();
},
);
test("registers the sandbox backend on Windows when feature probes pass", () => {
const { api, services } = createApi({ timeoutSeconds: 60 });
registerMxcPlugin(api);
expect(resolveMxcBinaryPathMock).toHaveBeenCalledWith(undefined);
expect(assertMxcReadinessMock).toHaveBeenCalledWith();
expect(warnMxcHostPrepIfNeededMock).toHaveBeenCalledWith();
expect(createMxcSandboxBackendFactoryMock).toHaveBeenCalledWith(
expect.objectContaining({
timeoutSeconds: 60,
}),
);
expect(registerSandboxBackendMock).toHaveBeenCalledWith("mxc", {
factory: factoryMock,
manager: mxcSandboxBackendManagerMock,
});
expect(services).toHaveLength(1);
void services[0]?.stop?.({} as OpenClawPluginServiceContext);
expect(unregisterMock).toHaveBeenCalledTimes(1);
});
test("keeps the existing binary-resolution failure path after host support passes", () => {
resolveMxcBinaryPathMock.mockImplementation(() => {
throw new Error("missing binary");
});
const { api, registerService } = createApi();
expect(() => registerMxcPlugin(api)).toThrow(
"[mxc] MXC sandbox backend cannot load: missing binary. Install @microsoft/mxc-sdk or set mxcBinaryPath.",
);
expect(warnSpy).not.toHaveBeenCalled();
expect(assertMxcReadinessMock).not.toHaveBeenCalled();
expect(registerSandboxBackendMock).not.toHaveBeenCalled();
expect(registerService).not.toHaveBeenCalled();
});
});