























@@ -18,13 +18,17 @@ function makeTempDir() {
18181919const mkdirSafe = mkdirSafeDir;
202021+function symlinkDirectory(target: string, linkPath: string): void {
22+fs.symlinkSync(target, linkPath, process.platform === "win32" ? "junction" : "dir");
23+}
24+2125const canCreateDirectorySymlinks = (() => {
2226const probeDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-symlink-probe-"));
2327const targetDir = path.join(probeDir, "target");
2428const linkDir = path.join(probeDir, "link");
2529try {
2630fs.mkdirSync(targetDir);
27-fs.symlinkSync(targetDir, linkDir, process.platform === "win32" ? "junction" : "dir");
31+symlinkDirectory(targetDir, linkDir);
2832return true;
2933} catch {
3034return false;
@@ -318,6 +322,72 @@ describe("discoverOpenClawPlugins", () => {
318322expectCandidateIds(candidates, { includes: ["alpha", "beta"] });
319323});
320324325+it.skipIf(!canCreateDirectorySymlinks)(
326+"discovers symlinked plugin directories in global roots",
327+async () => {
328+const stateDir = makeTempDir();
329+const globalExt = path.join(stateDir, "extensions");
330+mkdirSafe(globalExt);
331+332+const linkedPluginDir = path.join(stateDir, "linked-plugin-src");
333+createPackagePluginWithEntry({
334+packageDir: linkedPluginDir,
335+packageName: "@openclaw/linked-plugin",
336+pluginId: "linked-plugin",
337+});
338+339+symlinkDirectory(linkedPluginDir, path.join(globalExt, "linked-plugin"));
340+341+const { candidates, diagnostics } = await discoverWithStateDir(stateDir, {});
342+expectCandidateIds(candidates, { includes: ["linked-plugin"] });
343+expect(findCandidateById(candidates, "linked-plugin")?.rootDir).toBe(
344+fs.realpathSync(linkedPluginDir),
345+);
346+expect(diagnostics).toEqual([]);
347+},
348+);
349+350+it.skipIf(!canCreateDirectorySymlinks)(
351+"discovers symlinked plugin directories in workspace roots",
352+async () => {
353+const stateDir = makeTempDir();
354+const workspaceDir = path.join(stateDir, "workspace");
355+const workspaceExt = path.join(workspaceDir, ".openclaw", "extensions");
356+mkdirSafe(workspaceExt);
357+358+const linkedPluginDir = path.join(stateDir, "workspace-linked-plugin-src");
359+createPackagePluginWithEntry({
360+packageDir: linkedPluginDir,
361+packageName: "@openclaw/workspace-linked-plugin",
362+pluginId: "workspace-linked-plugin",
363+});
364+365+symlinkDirectory(linkedPluginDir, path.join(workspaceExt, "workspace-linked-plugin"));
366+367+const { candidates, diagnostics } = await discoverWithStateDir(stateDir, { workspaceDir });
368+expectCandidateIds(candidates, { includes: ["workspace-linked-plugin"] });
369+expect(findCandidateById(candidates, "workspace-linked-plugin")?.rootDir).toBe(
370+fs.realpathSync(linkedPluginDir),
371+);
372+expect(diagnostics).toEqual([]);
373+},
374+);
375+376+it.skipIf(process.platform === "win32" || !canCreateDirectorySymlinks)(
377+"ignores broken symlinked plugin directories in scanned roots",
378+async () => {
379+const stateDir = makeTempDir();
380+const globalExt = path.join(stateDir, "extensions");
381+mkdirSafe(globalExt);
382+383+symlinkDirectory(path.join(stateDir, "missing-plugin-src"), path.join(globalExt, "missing"));
384+385+const { candidates, diagnostics } = await discoverWithStateDir(stateDir, {});
386+expectCandidateIds(candidates, { excludes: ["missing"] });
387+expect(diagnostics).toEqual([]);
388+},
389+);
390+321391it("does not recurse arbitrary workspace directories for plugin auto-discovery", () => {
322392const stateDir = makeTempDir();
323393const workspaceDir = path.join(stateDir, "workspace");
@@ -631,11 +701,7 @@ describe("discoverOpenClawPlugins", () => {
631701writePluginEntry(path.join(realPackageDir, "src", "index.ts"));
632702633703const linkedPackageDir = path.join(stateDir, "linked-pack");
634-fs.symlinkSync(
635-realPackageDir,
636-linkedPackageDir,
637-process.platform === "win32" ? "junction" : "dir",
638-);
704+symlinkDirectory(realPackageDir, linkedPackageDir);
639705const canonicalPackageDir = fs.realpathSync(realPackageDir);
640706641707const realpathSync = vi.spyOn(fs, "realpathSync");
@@ -1100,7 +1166,7 @@ describe("discoverOpenClawPlugins", () => {
11001166mkdirSafe(outsideDir);
11011167fs.writeFileSync(path.join(outsideDir, "escape.ts"), "export default {}", "utf-8");
11021168try {
1103-fs.symlinkSync(outsideDir, linkedDir, process.platform === "win32" ? "junction" : "dir");
1169+symlinkDirectory(outsideDir, linkedDir);
11041170} catch {
11051171return false;
11061172}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。