



























@@ -1,4 +1,7 @@
1-import { beforeEach, describe, expect, it, vi } from "vitest";
1+import fs from "node:fs";
2+import os from "node:os";
3+import path from "node:path";
4+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
25import { resolveRegistryUpdateChannel } from "../../../infra/update-channels.js";
36import { resolveNpmInstallSpecsForUpdateChannel } from "../../../plugins/install-channel-specs.js";
47import { VERSION } from "../../../version.js";
@@ -48,11 +51,40 @@ const mocks = vi.hoisted(() => ({
4851(entry: { label?: string; id?: string }) => entry.label ?? entry.id ?? "plugin",
4952),
5053resolveDefaultPluginExtensionsDir: vi.fn(() => "/tmp/openclaw-plugins"),
54+resolvePluginInstallDir: vi.fn(
55+(pluginId: string, extensionsDir = "/tmp/openclaw-plugins") => `${extensionsDir}/${pluginId}`,
56+),
57+validatePluginId: vi.fn(() => null),
5158resolveProviderInstallCatalogEntries: vi.fn(),
5259updateNpmInstalledPlugins: vi.fn(),
5360writePersistedInstalledPluginIndexInstallRecords: vi.fn(),
5461}));
556263+const tempDirs: string[] = [];
64+65+function makeTempDir(): string {
66+const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-stub-repair-"));
67+tempDirs.push(dir);
68+return dir;
69+}
70+71+function writeLegacyNpmDeclarationStub(params: {
72+pluginDir: string;
73+pluginId: string;
74+npmSpec: string;
75+}): void {
76+fs.mkdirSync(params.pluginDir, { recursive: true });
77+fs.writeFileSync(
78+path.join(params.pluginDir, "openclaw.extension.json"),
79+JSON.stringify({
80+name: params.pluginId,
81+type: "npm",
82+npmSpec: params.npmSpec,
83+}),
84+"utf8",
85+);
86+}
87+5688vi.mock("../../../channels/plugins/catalog.js", () => ({
5789listChannelPluginCatalogEntries: mocks.listChannelPluginCatalogEntries,
5890}));
@@ -70,6 +102,8 @@ vi.mock("../../../plugins/installed-plugin-index.js", async (importOriginal) =>
7010271103vi.mock("../../../plugins/install-paths.js", () => ({
72104resolveDefaultPluginExtensionsDir: mocks.resolveDefaultPluginExtensionsDir,
105+resolvePluginInstallDir: mocks.resolvePluginInstallDir,
106+validatePluginId: mocks.validatePluginId,
73107}));
7410875109vi.mock("../../../plugins/install.js", () => ({
@@ -155,6 +189,12 @@ describe("repairMissingConfiguredPluginInstalls", () => {
155189});
156190});
157191192+afterEach(() => {
193+for (const dir of tempDirs.splice(0)) {
194+fs.rmSync(dir, { recursive: true, force: true });
195+}
196+});
197+158198it("installs a missing configured OpenClaw channel plugin from npm by default", async () => {
159199mocks.listChannelPluginCatalogEntries.mockReturnValue([
160200{
@@ -2311,6 +2351,66 @@ describe("repairMissingConfiguredPluginInstalls", () => {
23112351]);
23122352});
231323532354+it("repairs a configured plugin from a legacy npm declaration stub", async () => {
2355+const root = makeTempDir();
2356+const pluginDir = path.join(root, "extensions", "guardrail-bridge");
2357+writeLegacyNpmDeclarationStub({
2358+ pluginDir,
2359+pluginId: "guardrail-bridge",
2360+npmSpec: "@guardrail-bridge/guardrail-bridge@1.0.0",
2361+});
2362+mocks.installPluginFromNpmSpec.mockResolvedValueOnce({
2363+ok: true,
2364+pluginId: "guardrail-bridge",
2365+targetDir: "/tmp/openclaw-plugins/guardrail-bridge",
2366+version: "1.0.0",
2367+npmResolution: {
2368+name: "@guardrail-bridge/guardrail-bridge",
2369+version: "1.0.0",
2370+resolvedSpec: "@guardrail-bridge/guardrail-bridge@1.0.0",
2371+integrity: "sha512-guardrail",
2372+resolvedAt: "2026-05-01T00:00:00.000Z",
2373+},
2374+});
2375+2376+const { repairMissingConfiguredPluginInstalls } =
2377+await import("./missing-configured-plugin-install.js");
2378+const result = await repairMissingConfiguredPluginInstalls({
2379+cfg: {
2380+plugins: {
2381+load: {
2382+paths: [pluginDir],
2383+},
2384+entries: {
2385+"guardrail-bridge": { enabled: true },
2386+},
2387+},
2388+},
2389+env: {},
2390+});
2391+2392+expectRecordFields(mockCallArg(mocks.installPluginFromNpmSpec), {
2393+spec: "@guardrail-bridge/guardrail-bridge@1.0.0",
2394+expectedPluginId: "guardrail-bridge",
2395+extensionsDir: "/tmp/openclaw-plugins",
2396+});
2397+expect(mockCallArg(mocks.installPluginFromNpmSpec).trustedSourceLinkedOfficialInstall).toBe(
2398+undefined,
2399+);
2400+const records = mockCallArg(mocks.writePersistedInstalledPluginIndexInstallRecords);
2401+expectRecordFields((records as Record<string, unknown>)["guardrail-bridge"], {
2402+source: "npm",
2403+spec: "@guardrail-bridge/guardrail-bridge@1.0.0",
2404+installPath: "/tmp/openclaw-plugins/guardrail-bridge",
2405+version: "1.0.0",
2406+resolvedName: "@guardrail-bridge/guardrail-bridge",
2407+});
2408+expect(result.changes).toEqual([
2409+'Installed missing configured plugin "guardrail-bridge" from @guardrail-bridge/guardrail-bridge@1.0.0.',
2410+]);
2411+expect(result.warnings).toStrictEqual([]);
2412+});
2413+23142414it("does not install a configured external web search plugin when search is disabled", async () => {
23152415mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([
23162416{
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。