




















@@ -9,6 +9,7 @@ import { expectInstallUsesIgnoreScripts } from "../test-utils/npm-spec-install-t
99import { initializeGlobalHookRunner, resetGlobalHookRunner } from "./hook-runner-global.js";
1010import { createMockPluginRegistry } from "./hooks.test-helpers.js";
1111import * as installSecurityScan from "./install-security-scan.js";
12+import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
1213import {
1314installPluginFromArchive,
1415installPluginFromDir,
@@ -21,6 +22,10 @@ vi.mock("../process/exec.js", () => ({
2122runCommandWithTimeout: vi.fn(),
2223}));
232425+vi.mock("../infra/openclaw-root.js", () => ({
26+resolveOpenClawPackageRootSync: vi.fn(),
27+}));
28+2429const resolveCompatibilityHostVersionMock = vi.fn();
25302631vi.mock("./install.runtime.js", async () => {
@@ -2350,3 +2355,96 @@ describe("installPluginFromDir", () => {
23502355});
23512356});
23522357});
2358+2359+describe("linkOpenClawPeerDependencies (via installPluginFromDir)", () => {
2360+const resolveRootMock = vi.mocked(resolveOpenClawPackageRootSync);
2361+2362+function writePluginWithPeerDeps(
2363+pluginDir: string,
2364+peerDependencies: Record<string, string>,
2365+): void {
2366+fs.mkdirSync(pluginDir, { recursive: true });
2367+fs.writeFileSync(
2368+path.join(pluginDir, "package.json"),
2369+JSON.stringify({
2370+name: "peer-dep-plugin",
2371+version: "1.0.0",
2372+openclaw: { extensions: ["index.js"] },
2373+ peerDependencies,
2374+}),
2375+"utf-8",
2376+);
2377+fs.writeFileSync(path.join(pluginDir, "index.js"), "export {};\n", "utf-8");
2378+}
2379+2380+it("creates a node_modules/openclaw symlink when peerDependencies declares openclaw", async () => {
2381+const { pluginDir, extensionsDir } = setupPluginInstallDirs();
2382+const fakeHostRoot = suiteTempRootTracker.makeTempDir();
2383+resolveRootMock.mockReturnValue(fakeHostRoot);
2384+2385+writePluginWithPeerDeps(pluginDir, { openclaw: "*" });
2386+2387+const { result } = await installFromDirWithWarnings({ pluginDir, extensionsDir });
2388+2389+expect(result.ok).toBe(true);
2390+if (!result.ok) return;
2391+2392+const symlinkPath = path.join(result.targetDir, "node_modules", "openclaw");
2393+const stat = fs.lstatSync(symlinkPath);
2394+expect(stat.isSymbolicLink()).toBe(true);
2395+expect(fs.realpathSync(symlinkPath)).toBe(fs.realpathSync(fakeHostRoot));
2396+});
2397+2398+it("does not create a symlink when peerDependencies is empty", async () => {
2399+const { pluginDir, extensionsDir } = setupPluginInstallDirs();
2400+resolveRootMock.mockReturnValue(suiteTempRootTracker.makeTempDir());
2401+2402+writePluginWithPeerDeps(pluginDir, {});
2403+2404+const { result } = await installFromDirWithWarnings({ pluginDir, extensionsDir });
2405+2406+expect(result.ok).toBe(true);
2407+if (!result.ok) return;
2408+2409+const nodeModulesDir = path.join(result.targetDir, "node_modules");
2410+const symlinkPath = path.join(nodeModulesDir, "openclaw");
2411+expect(fs.existsSync(symlinkPath)).toBe(false);
2412+});
2413+2414+it("is idempotent — re-installing replaces an existing symlink without error", async () => {
2415+const { pluginDir, extensionsDir } = setupPluginInstallDirs();
2416+const fakeHostRoot = suiteTempRootTracker.makeTempDir();
2417+resolveRootMock.mockReturnValue(fakeHostRoot);
2418+2419+writePluginWithPeerDeps(pluginDir, { openclaw: "*" });
2420+2421+// First install
2422+const { result: first } = await installFromDirWithWarnings({ pluginDir, extensionsDir });
2423+expect(first.ok).toBe(true);
2424+2425+// Second install (update mode) — should replace symlink, not throw
2426+const { result: second, warnings } = await installFromDirWithWarnings({
2427+ pluginDir,
2428+ extensionsDir,
2429+mode: "update",
2430+});
2431+expect(second.ok).toBe(true);
2432+expect(warnings).toHaveLength(0);
2433+2434+if (!second.ok) return;
2435+const symlinkPath = path.join(second.targetDir, "node_modules", "openclaw");
2436+expect(fs.lstatSync(symlinkPath).isSymbolicLink()).toBe(true);
2437+});
2438+2439+it("warns and skips when resolveOpenClawPackageRootSync returns null", async () => {
2440+const { pluginDir, extensionsDir } = setupPluginInstallDirs();
2441+resolveRootMock.mockReturnValue(null);
2442+2443+writePluginWithPeerDeps(pluginDir, { openclaw: "*" });
2444+2445+const { result, warnings } = await installFromDirWithWarnings({ pluginDir, extensionsDir });
2446+2447+expect(result.ok).toBe(true);
2448+expect(warnings.some((w) => w.includes("Could not locate openclaw package root"))).toBe(true);
2449+});
2450+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。