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 { // Keep workspace dependencies resolvable from the copied integration package. const root = fs.mkdtempSync(path.resolve(".linux-node-plugin-test-")); try { // Bundled discovery requires the package itself to stay inside its physical root. 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); } }); });