





















@@ -0,0 +1,159 @@
1+import fs from "node:fs";
2+import { createRequire } from "node:module";
3+import os from "node:os";
4+import path from "node:path";
5+import { afterEach, describe, expect, it } from "vitest";
6+import {
7+installOpenClawPluginSdkNativeResolver,
8+resetOpenClawPluginSdkNativeResolverForTest,
9+} from "./plugin-sdk-native-resolver.js";
10+11+afterEach(() => {
12+resetOpenClawPluginSdkNativeResolverForTest();
13+});
14+15+function writeJsonFile(targetPath: string, value: unknown): void {
16+fs.mkdirSync(path.dirname(targetPath), { recursive: true });
17+fs.writeFileSync(targetPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
18+}
19+20+function writeFakeOpenClawPackage(root: string): { distRoot: string; loaderModulePath: string } {
21+writeJsonFile(path.join(root, "package.json"), {
22+name: "openclaw",
23+type: "module",
24+bin: {
25+openclaw: "./openclaw.mjs",
26+},
27+exports: {
28+"./cli-entry": "./dist/cli-entry.js",
29+"./plugin-sdk": "./dist/plugin-sdk/root-alias.cjs",
30+"./plugin-sdk/channel-message": "./dist/plugin-sdk/channel-message.js",
31+"./plugin-sdk/source-only": "./dist/plugin-sdk/source-only.js",
32+},
33+});
34+fs.writeFileSync(path.join(root, "openclaw.mjs"), "#!/usr/bin/env node\n", "utf8");
35+const distRoot = path.join(root, "dist");
36+const pluginSdkDir = path.join(distRoot, "plugin-sdk");
37+fs.mkdirSync(pluginSdkDir, { recursive: true });
38+fs.writeFileSync(path.join(pluginSdkDir, "root-alias.cjs"), "module.exports = {};\n", "utf8");
39+fs.writeFileSync(
40+path.join(pluginSdkDir, "channel-message.js"),
41+['export const defineChannelMessageAdapter = () => "adapter";', ""].join("\n"),
42+"utf8",
43+);
44+const loaderModulePath = path.join(distRoot, "plugins", "loader.js");
45+fs.mkdirSync(path.dirname(loaderModulePath), { recursive: true });
46+fs.writeFileSync(loaderModulePath, "export default {};\n", "utf8");
47+return { distRoot, loaderModulePath };
48+}
49+50+function writeExternalPluginEntry(root: string): string {
51+writeJsonFile(path.join(root, "package.json"), {
52+name: "external-plugin",
53+type: "module",
54+});
55+const entry = path.join(root, "dist", "runtime-api.js");
56+fs.mkdirSync(path.dirname(entry), { recursive: true });
57+fs.writeFileSync(entry, "export default {};\n", "utf8");
58+return entry;
59+}
60+61+describe("installOpenClawPluginSdkNativeResolver", () => {
62+it("keeps native aliases on JS dist artifacts when source files exist", () => {
63+const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sdk-native-source-resolver-"));
64+const { loaderModulePath } = writeFakeOpenClawPackage(root);
65+const sourceChannelMessagePath = path.join(root, "src", "plugin-sdk", "channel-message.ts");
66+fs.mkdirSync(path.dirname(sourceChannelMessagePath), { recursive: true });
67+fs.writeFileSync(sourceChannelMessagePath, "export const sourceOnly = true;\n", "utf8");
68+const externalPluginEntry = writeExternalPluginEntry(path.join(root, "external-plugin"));
69+70+const installedAliases = installOpenClawPluginSdkNativeResolver({
71+modulePath: loaderModulePath,
72+pluginModulePath: externalPluginEntry,
73+pluginSdkResolution: "src",
74+});
75+76+expect(installedAliases).toContain("openclaw/plugin-sdk/channel-message");
77+const requireFromPlugin = createRequire(externalPluginEntry);
78+expect(fs.realpathSync(requireFromPlugin.resolve("openclaw/plugin-sdk/channel-message"))).toBe(
79+fs.realpathSync(path.join(root, "dist", "plugin-sdk", "channel-message.js")),
80+);
81+});
82+83+it("lets built external plugins resolve OpenClaw SDK subpaths with createRequire", () => {
84+const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sdk-native-resolver-"));
85+const { distRoot, loaderModulePath } = writeFakeOpenClawPackage(root);
86+const externalPluginEntry = writeExternalPluginEntry(path.join(root, "external-plugin"));
87+88+const distMode = fs.statSync(distRoot).mode;
89+if (process.platform !== "win32") {
90+fs.chmodSync(distRoot, 0o555);
91+}
92+93+try {
94+const installedAliases = installOpenClawPluginSdkNativeResolver({
95+modulePath: loaderModulePath,
96+pluginModulePath: externalPluginEntry,
97+pluginSdkResolution: "dist",
98+});
99+100+expect(installedAliases).toContain("openclaw/plugin-sdk/channel-message");
101+expect(fs.existsSync(path.join(distRoot, "extensions"))).toBe(false);
102+const requireFromPlugin = createRequire(externalPluginEntry);
103+expect(
104+fs.realpathSync(requireFromPlugin.resolve("openclaw/plugin-sdk/channel-message")),
105+).toBe(fs.realpathSync(path.join(root, "dist", "plugin-sdk", "channel-message.js")));
106+const sdk = requireFromPlugin("openclaw/plugin-sdk/channel-message") as {
107+defineChannelMessageAdapter?: () => string;
108+};
109+110+expect(sdk.defineChannelMessageAdapter?.()).toBe("adapter");
111+expect(() => requireFromPlugin.resolve("openclaw/not-plugin-sdk/channel-message")).toThrow();
112+} finally {
113+if (process.platform !== "win32") {
114+fs.chmodSync(distRoot, distMode);
115+}
116+}
117+});
118+119+it("does not resolve SDK aliases for parents outside registered plugin roots", () => {
120+const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sdk-native-guard-"));
121+const { loaderModulePath } = writeFakeOpenClawPackage(root);
122+const externalPluginEntry = writeExternalPluginEntry(path.join(root, "external-plugin"));
123+const unrelatedRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sdk-native-outside-"));
124+const unrelatedEntry = path.join(unrelatedRoot, "runtime-api.js");
125+fs.mkdirSync(path.dirname(unrelatedEntry), { recursive: true });
126+fs.writeFileSync(unrelatedEntry, "export default {};\n", "utf8");
127+128+installOpenClawPluginSdkNativeResolver({
129+modulePath: loaderModulePath,
130+pluginModulePath: externalPluginEntry,
131+pluginSdkResolution: "dist",
132+});
133+134+const requireFromPlugin = createRequire(externalPluginEntry);
135+const requireFromOutside = createRequire(unrelatedEntry);
136+expect(requireFromPlugin.resolve("openclaw/plugin-sdk/channel-message")).toBeTruthy();
137+expect(() => requireFromOutside.resolve("openclaw/plugin-sdk/channel-message")).toThrow();
138+});
139+140+it("does not register source-only SDK subpaths for native resolution", () => {
141+const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sdk-native-source-only-"));
142+const { loaderModulePath } = writeFakeOpenClawPackage(root);
143+const sourceOnlyPath = path.join(root, "src", "plugin-sdk", "source-only.ts");
144+fs.mkdirSync(path.dirname(sourceOnlyPath), { recursive: true });
145+fs.writeFileSync(sourceOnlyPath, "export const sourceOnly = true;\n", "utf8");
146+const externalPluginEntry = writeExternalPluginEntry(path.join(root, "external-plugin"));
147+148+const installedAliases = installOpenClawPluginSdkNativeResolver({
149+modulePath: loaderModulePath,
150+pluginModulePath: externalPluginEntry,
151+pluginSdkResolution: "src",
152+});
153+154+expect(installedAliases).toContain("openclaw/plugin-sdk/channel-message");
155+expect(installedAliases).not.toContain("openclaw/plugin-sdk/source-only");
156+const requireFromPlugin = createRequire(externalPluginEntry);
157+expect(() => requireFromPlugin.resolve("openclaw/plugin-sdk/source-only")).toThrow();
158+});
159+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。