| import fs from "node:fs"; |
| import path from "node:path"; |
| import { afterEach, describe, expect, it, vi } from "vitest"; |
| import type { OpenClawConfig } from "../config/types.openclaw.js"; |
| import { reconcileNodePairingOnConnect } from "../gateway/node-connect-reconcile.js"; |
| import { resetPluginLoaderTestStateForTest } from "../plugins/loader.test-fixtures.js"; |
| import { setActivePluginRegistry } from "../plugins/runtime.js"; |
| import { listRegisteredNodeHostCapsAndCommands } from "./plugin-node-host.js"; |
| import { |
| getNodeHostPluginRegistry, |
| resetNodeHostPluginRegistry, |
| } from "./plugin-node-host.test-support.js"; |
| import { prepareNodeHostRuntime } from "./runtime.js"; |
|
|
| const LINUX_NODE_COMMANDS = [ |
| "camera.clip", |
| "camera.list", |
| "camera.snap", |
| "location.get", |
| "system.notify", |
| ] as const; |
|
|
| function resetPluginState(): void { |
| resetPluginLoaderTestStateForTest(); |
| resetNodeHostPluginRegistry(); |
| } |
|
|
| const tempBundledRoots: string[] = []; |
|
|
| function createLinuxNodeBundledRoot(): string { |
| |
| const root = fs.mkdtempSync(path.resolve(".linux-node-plugin-test-")); |
| try { |
| |
| fs.cpSync(path.resolve("extensions/linux-node"), path.join(root, "linux-node"), { |
| recursive: true, |
| }); |
| } catch (error) { |
| fs.rmSync(root, { force: true, recursive: true }); |
| throw error; |
| } |
| tempBundledRoots.push(root); |
| return root; |
| } |
|
|
| afterEach(() => { |
| vi.restoreAllMocks(); |
| vi.unstubAllEnvs(); |
| resetPluginState(); |
| for (const root of tempBundledRoots.splice(0)) { |
| fs.rmSync(root, { force: true, recursive: true }); |
| } |
| }); |
|
|
| describe("linux-node node-host integration", () => { |
| it("loads and advertises enabled commands through the node-host runtime", async () => { |
| resetPluginState(); |
| const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform"); |
| if (!platformDescriptor) { |
| throw new Error("process.platform descriptor unavailable"); |
| } |
| const bundledRoot = createLinuxNodeBundledRoot(); |
| Object.defineProperty(process, "platform", { ...platformDescriptor, value: "linux" }); |
|
|
| const fakeBinDir = path.resolve(".artifacts", "linux-node-test-bin"); |
| const originalAccessSync = fs.accessSync.bind(fs); |
| vi.spyOn(fs, "accessSync").mockImplementation((candidate, mode) => { |
| if (path.dirname(path.resolve(String(candidate))) === fakeBinDir) { |
| return; |
| } |
| return originalAccessSync(candidate, mode); |
| }); |
| vi.stubEnv("PATH", `${fakeBinDir}${path.delimiter}${process.env.PATH ?? ""}`); |
| vi.stubEnv("OPENCLAW_BUNDLED_PLUGINS_DIR", bundledRoot); |
| vi.stubEnv("OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR", "1"); |
| vi.stubEnv("OPENCLAW_DISABLE_BUNDLED_PLUGINS", undefined); |
|
|
| const config: OpenClawConfig = { |
| gateway: { |
| nodes: { |
| commands: { allow: ["camera.snap", "camera.clip"] }, |
| }, |
| }, |
| nodeHost: { skills: { enabled: false } }, |
| plugins: { |
| allow: ["linux-node"], |
| entries: { |
| "linux-node": { |
| enabled: true, |
| config: { |
| notify: { enabled: true }, |
| camera: { enabled: true }, |
| location: { enabled: true }, |
| }, |
| }, |
| }, |
| }, |
| }; |
|
|
| try { |
| const prepared = await prepareNodeHostRuntime({ config, env: process.env }); |
| const registered = listRegisteredNodeHostCapsAndCommands({ config, env: process.env }); |
|
|
| expect(registered.commands, JSON.stringify(getNodeHostPluginRegistry()?.diagnostics)).toEqual( |
| LINUX_NODE_COMMANDS, |
| ); |
| expect(registered.caps).toEqual(["camera", "location"]); |
| expect(prepared.manifest.commands).toEqual(expect.arrayContaining([...LINUX_NODE_COMMANDS])); |
|
|
| const requestPairing = vi.fn(); |
| setActivePluginRegistry(getNodeHostPluginRegistry()!); |
| const reconciliation = await reconcileNodePairingOnConnect({ |
| cfg: config, |
| connectParams: { |
| minProtocol: 1, |
| maxProtocol: 1, |
| client: { |
| id: "node-host", |
| version: "test", |
| platform: "linux", |
| deviceFamily: "Linux", |
| mode: "node", |
| }, |
| caps: registered.caps, |
| commands: registered.commands, |
| }, |
| pairedNode: { |
| nodeId: "node-host", |
| createdAtMs: 1, |
| approvedAtMs: 1, |
| caps: registered.caps, |
| commands: registered.commands, |
| }, |
| requestPairing, |
| }); |
|
|
| expect(reconciliation.declaredCommands).toEqual(LINUX_NODE_COMMANDS); |
| expect(reconciliation.effectiveCommands).toEqual(LINUX_NODE_COMMANDS); |
| expect(requestPairing).not.toHaveBeenCalled(); |
| } finally { |
| Object.defineProperty(process, "platform", platformDescriptor); |
| } |
| }); |
| }); |
|
|