




















@@ -60,6 +60,88 @@ function createCandidate(rootDir: string, id = "demo"): PluginCandidate {
6060};
6161}
626263+function createBundledCandidate(params: {
64+rootDir: string;
65+id: string;
66+packageName: string;
67+version: string;
68+}): PluginCandidate {
69+fs.writeFileSync(
70+path.join(params.rootDir, "index.ts"),
71+"throw new Error('runtime entry should not load during doctor registry repair');\n",
72+"utf8",
73+);
74+fs.writeFileSync(
75+path.join(params.rootDir, "openclaw.plugin.json"),
76+JSON.stringify({
77+id: params.id,
78+name: params.id,
79+configSchema: { type: "object" },
80+providers: [params.id],
81+}),
82+"utf8",
83+);
84+fs.writeFileSync(
85+path.join(params.rootDir, "package.json"),
86+JSON.stringify({
87+name: params.packageName,
88+version: params.version,
89+}),
90+"utf8",
91+);
92+return {
93+idHint: params.id,
94+source: path.join(params.rootDir, "index.ts"),
95+rootDir: params.rootDir,
96+origin: "bundled",
97+packageName: params.packageName,
98+packageVersion: params.version,
99+};
100+}
101+102+function createManagedNpmPlugin(params: {
103+stateDir: string;
104+id: string;
105+packageName: string;
106+version: string;
107+}) {
108+const npmRoot = path.join(params.stateDir, "npm");
109+const packageDir = path.join(npmRoot, "node_modules", params.packageName);
110+fs.mkdirSync(packageDir, { recursive: true });
111+fs.writeFileSync(
112+path.join(npmRoot, "package.json"),
113+JSON.stringify({
114+dependencies: {
115+[params.packageName]: params.version,
116+},
117+}),
118+"utf8",
119+);
120+fs.writeFileSync(
121+path.join(packageDir, "package.json"),
122+JSON.stringify({
123+name: params.packageName,
124+version: params.version,
125+openclaw: {
126+extensions: ["."],
127+},
128+}),
129+"utf8",
130+);
131+fs.writeFileSync(
132+path.join(packageDir, "openclaw.plugin.json"),
133+JSON.stringify({
134+id: params.id,
135+name: params.id,
136+configSchema: {
137+type: "object",
138+},
139+}),
140+"utf8",
141+);
142+return { npmRoot, packageDir };
143+}
144+63145function createCurrentIndex(): InstalledPluginIndex {
64146return {
65147version: 1,
@@ -115,4 +197,108 @@ describe("maybeRepairPluginRegistryState", () => {
115197expect(nextConfig).toEqual({});
116198expect(vi.mocked(note).mock.calls.join("\n")).toContain(DISABLE_PLUGIN_REGISTRY_MIGRATION_ENV);
117199});
200+201+it("warns about stale managed npm packages that shadow bundled plugins", async () => {
202+const stateDir = makeTempDir();
203+const bundledDir = path.join(stateDir, "bundled", "google-meet");
204+fs.mkdirSync(bundledDir, { recursive: true });
205+createManagedNpmPlugin({
206+ stateDir,
207+id: "google-meet",
208+packageName: "@openclaw/google-meet",
209+version: "2026.5.2",
210+});
211+await writePersistedInstalledPluginIndex(createCurrentIndex(), { stateDir });
212+213+await maybeRepairPluginRegistryState({
214+ stateDir,
215+candidates: [
216+createBundledCandidate({
217+rootDir: bundledDir,
218+id: "google-meet",
219+packageName: "@openclaw/google-meet",
220+version: "2026.5.3",
221+}),
222+],
223+env: hermeticEnv(),
224+config: {
225+plugins: {
226+allow: ["google-meet"],
227+entries: {
228+"google-meet": {
229+enabled: true,
230+config: {},
231+},
232+},
233+},
234+},
235+prompter: { shouldRepair: false },
236+});
237+238+expect(vi.mocked(note).mock.calls.join("\n")).toContain(
239+"Managed npm plugin packages shadow bundled plugins",
240+);
241+expect(vi.mocked(note).mock.calls.join("\n")).toContain("@openclaw/google-meet@2026.5.2");
242+expect(
243+fs.existsSync(path.join(stateDir, "npm", "node_modules", "@openclaw", "google-meet")),
244+).toBe(true);
245+});
246+247+it("removes stale managed npm packages that shadow bundled plugins during repair", async () => {
248+const stateDir = makeTempDir();
249+const bundledDir = path.join(stateDir, "bundled", "google-meet");
250+fs.mkdirSync(bundledDir, { recursive: true });
251+createManagedNpmPlugin({
252+ stateDir,
253+id: "google-meet",
254+packageName: "@openclaw/google-meet",
255+version: "2026.5.2",
256+});
257+await writePersistedInstalledPluginIndex(createCurrentIndex(), { stateDir });
258+259+await maybeRepairPluginRegistryState({
260+ stateDir,
261+candidates: [
262+createBundledCandidate({
263+rootDir: bundledDir,
264+id: "google-meet",
265+packageName: "@openclaw/google-meet",
266+version: "2026.5.3",
267+}),
268+],
269+env: hermeticEnv(),
270+config: {
271+plugins: {
272+allow: ["google-meet"],
273+entries: {
274+"google-meet": {
275+enabled: true,
276+config: {},
277+},
278+},
279+},
280+},
281+prompter: { shouldRepair: true },
282+});
283+284+expect(
285+fs.existsSync(path.join(stateDir, "npm", "node_modules", "@openclaw", "google-meet")),
286+).toBe(false);
287+expect(
288+JSON.parse(fs.readFileSync(path.join(stateDir, "npm", "package.json"), "utf8")),
289+).not.toHaveProperty("dependencies");
290+await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject({
291+refreshReason: "migration",
292+plugins: [
293+expect.objectContaining({
294+pluginId: "google-meet",
295+origin: "bundled",
296+rootDir: bundledDir,
297+}),
298+],
299+});
300+expect(vi.mocked(note).mock.calls.join("\n")).toContain(
301+"Removed stale managed npm plugin package",
302+);
303+});
118304});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。