mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 11:23:57 +00:00
220 lines
6.8 KiB
TypeScript
220 lines
6.8 KiB
TypeScript
/** Tests node-host runner command parsing, timeout, and plugin dispatch behavior. */
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import type { GatewayClientOptions } from "../gateway/client.js";
|
|
import {
|
|
resolveNodeHostGatewayDeviceFamily,
|
|
resolveNodeHostGatewayPlatform,
|
|
runNodeHost,
|
|
} from "./runner.js";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
capturedGatewayClientOptions: [] as GatewayClientOptions[],
|
|
capturedSavedGatewayConfigs: [] as Array<{ contextPath?: string }>,
|
|
ensureNodeHostConfig: vi.fn(async () => ({
|
|
version: 1,
|
|
nodeId: "node-test",
|
|
})),
|
|
saveNodeHostConfig: vi.fn(async (cfg: { gateway?: { contextPath?: string } }) => {
|
|
if (cfg?.gateway) {
|
|
mocks.capturedSavedGatewayConfigs.push(cfg.gateway);
|
|
}
|
|
return undefined;
|
|
}),
|
|
getRuntimeConfig: vi.fn(() => ({
|
|
gateway: {
|
|
handshakeTimeoutMs: 1_000,
|
|
},
|
|
})),
|
|
startGatewayClientWhenEventLoopReady: vi.fn(async () => ({
|
|
ready: false,
|
|
aborted: false,
|
|
elapsedMs: 0,
|
|
})),
|
|
}));
|
|
|
|
vi.mock("../config/config.js", () => ({
|
|
getRuntimeConfig: mocks.getRuntimeConfig,
|
|
}));
|
|
|
|
vi.mock("../gateway/client-start-readiness.js", () => ({
|
|
startGatewayClientWhenEventLoopReady: mocks.startGatewayClientWhenEventLoopReady,
|
|
}));
|
|
|
|
vi.mock("../gateway/client.js", () => ({
|
|
GatewayClient: function GatewayClient(opts: GatewayClientOptions) {
|
|
mocks.capturedGatewayClientOptions.push(opts);
|
|
},
|
|
}));
|
|
|
|
vi.mock("../gateway/connection-auth.js", () => ({
|
|
resolveGatewayConnectionAuth: vi.fn(async () => ({})),
|
|
}));
|
|
|
|
vi.mock("../infra/device-identity.js", () => ({
|
|
loadOrCreateDeviceIdentity: vi.fn(() => ({
|
|
id: "device-test",
|
|
publicKey: "public-key-test",
|
|
privateKey: "private-key-test",
|
|
})),
|
|
}));
|
|
|
|
vi.mock("../infra/machine-name.js", () => ({
|
|
getMachineDisplayName: vi.fn(async () => "test-node"),
|
|
}));
|
|
|
|
vi.mock("../infra/path-env.js", () => ({
|
|
ensureOpenClawCliOnPath: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("./config.js", () => ({
|
|
ensureNodeHostConfig: mocks.ensureNodeHostConfig,
|
|
saveNodeHostConfig: mocks.saveNodeHostConfig,
|
|
}));
|
|
|
|
vi.mock("./plugin-node-host.js", () => ({
|
|
ensureNodeHostPluginRegistry: vi.fn(async () => undefined),
|
|
listRegisteredNodeHostCapsAndCommands: vi.fn(() => ({
|
|
caps: [],
|
|
commands: [],
|
|
})),
|
|
}));
|
|
|
|
function lastCapturedOptions(): GatewayClientOptions | undefined {
|
|
const list = mocks.capturedGatewayClientOptions;
|
|
return list[list.length - 1];
|
|
}
|
|
|
|
describe("runNodeHost", () => {
|
|
it("maps runtime platforms to gateway platform ids", () => {
|
|
expect(resolveNodeHostGatewayPlatform("darwin")).toBe("macos");
|
|
expect(resolveNodeHostGatewayPlatform("win32")).toBe("windows");
|
|
expect(resolveNodeHostGatewayPlatform("linux")).toBe("linux");
|
|
expect(resolveNodeHostGatewayPlatform("freebsd")).toBe("unknown");
|
|
expect(resolveNodeHostGatewayDeviceFamily("darwin")).toBe("Mac");
|
|
expect(resolveNodeHostGatewayDeviceFamily("win32")).toBe("Windows");
|
|
expect(resolveNodeHostGatewayDeviceFamily("linux")).toBe("Linux");
|
|
expect(resolveNodeHostGatewayDeviceFamily("freebsd")).toBeUndefined();
|
|
});
|
|
|
|
it("passes the resolved Gateway URL to the Gateway client", async () => {
|
|
await expect(
|
|
runNodeHost({
|
|
gatewayHost: "127.0.0.1",
|
|
gatewayPort: 18789,
|
|
}),
|
|
).rejects.toThrow("event loop readiness timeout");
|
|
|
|
expect(mocks.capturedGatewayClientOptions).toHaveLength(1);
|
|
expect(mocks.capturedGatewayClientOptions[0]?.url).toBe("ws://127.0.0.1:18789");
|
|
expect(mocks.capturedGatewayClientOptions[0]?.platform).toBe(
|
|
resolveNodeHostGatewayPlatform(process.platform),
|
|
);
|
|
expect(mocks.capturedGatewayClientOptions[0]?.deviceFamily).toBe(
|
|
resolveNodeHostGatewayDeviceFamily(process.platform),
|
|
);
|
|
});
|
|
|
|
it("appends context path to the Gateway WebSocket URL", async () => {
|
|
await expect(
|
|
runNodeHost({
|
|
gatewayHost: "127.0.0.1",
|
|
gatewayPort: 18789,
|
|
gatewayContextPath: "/gws",
|
|
}),
|
|
).rejects.toThrow("event loop readiness timeout");
|
|
|
|
expect(lastCapturedOptions()?.url).toBe("ws://127.0.0.1:18789/gws");
|
|
});
|
|
|
|
it("preserves trailing slash in context path as-is", async () => {
|
|
await expect(
|
|
runNodeHost({
|
|
gatewayHost: "127.0.0.1",
|
|
gatewayPort: 18789,
|
|
gatewayContextPath: "/gws/",
|
|
}),
|
|
).rejects.toThrow("event loop readiness timeout");
|
|
|
|
expect(lastCapturedOptions()?.url).toBe("ws://127.0.0.1:18789/gws/");
|
|
});
|
|
|
|
it("prepends leading slash when context path is missing one", async () => {
|
|
await expect(
|
|
runNodeHost({
|
|
gatewayHost: "127.0.0.1",
|
|
gatewayPort: 18789,
|
|
gatewayContextPath: "gws",
|
|
}),
|
|
).rejects.toThrow("event loop readiness timeout");
|
|
|
|
expect(lastCapturedOptions()?.url).toBe("ws://127.0.0.1:18789/gws");
|
|
});
|
|
|
|
it("omits context path when empty or undefined", async () => {
|
|
await expect(
|
|
runNodeHost({
|
|
gatewayHost: "127.0.0.1",
|
|
gatewayPort: 18789,
|
|
gatewayContextPath: "",
|
|
}),
|
|
).rejects.toThrow("event loop readiness timeout");
|
|
|
|
expect(lastCapturedOptions()?.url).toBe("ws://127.0.0.1:18789");
|
|
});
|
|
|
|
it("saves the gateway config with contextPath to node.json", async () => {
|
|
await expect(
|
|
runNodeHost({
|
|
gatewayHost: "127.0.0.1",
|
|
gatewayPort: 18789,
|
|
gatewayContextPath: "/gws",
|
|
}),
|
|
).rejects.toThrow("event loop readiness timeout");
|
|
|
|
const lastSaved =
|
|
mocks.capturedSavedGatewayConfigs[mocks.capturedSavedGatewayConfigs.length - 1];
|
|
expect(lastSaved?.contextPath).toBe("/gws");
|
|
});
|
|
|
|
it("clears saved contextPath when opts do not pass one (retarget scenario)", async () => {
|
|
mocks.ensureNodeHostConfig.mockResolvedValueOnce({
|
|
version: 1,
|
|
nodeId: "node-test",
|
|
gateway: { contextPath: "/old-path" },
|
|
} as any);
|
|
|
|
await expect(
|
|
runNodeHost({
|
|
gatewayHost: "192.168.1.1",
|
|
gatewayPort: 9999,
|
|
}),
|
|
).rejects.toThrow("event loop readiness timeout");
|
|
|
|
const lastSaved =
|
|
mocks.capturedSavedGatewayConfigs[mocks.capturedSavedGatewayConfigs.length - 1];
|
|
expect(lastSaved?.contextPath).toBeUndefined();
|
|
expect(lastCapturedOptions()?.url).toBe("ws://192.168.1.1:9999");
|
|
});
|
|
|
|
it("clears saved contextPath when explicitly passed as empty string", async () => {
|
|
mocks.ensureNodeHostConfig.mockResolvedValueOnce({
|
|
version: 1,
|
|
nodeId: "node-test",
|
|
gateway: { contextPath: "/old-path" },
|
|
} as any);
|
|
|
|
await expect(
|
|
runNodeHost({
|
|
gatewayHost: "127.0.0.1",
|
|
gatewayPort: 18789,
|
|
gatewayContextPath: "",
|
|
}),
|
|
).rejects.toThrow("event loop readiness timeout");
|
|
|
|
const lastSaved =
|
|
mocks.capturedSavedGatewayConfigs[mocks.capturedSavedGatewayConfigs.length - 1];
|
|
expect(lastSaved?.contextPath || undefined).toBeUndefined();
|
|
expect(lastCapturedOptions()?.url).toBe("ws://127.0.0.1:18789");
|
|
});
|
|
});
|