fix(doctor): repair allow-only official plugins

This commit is contained in:
Vincent Koc
2026-05-04 16:31:41 -07:00
committed by GitHub
parent d0c7f91ed1
commit ae142cad7c
3 changed files with 85 additions and 0 deletions

View File

@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
detectPluginAutoEnableCandidates: vi.fn(),
getOfficialExternalPluginCatalogEntry: vi.fn(),
repairMissingPluginInstallsForIds: vi.fn(),
resolveProviderInstallCatalogEntries: vi.fn(),
}));
@@ -14,6 +15,14 @@ vi.mock("../../../plugins/provider-install-catalog.js", () => ({
resolveProviderInstallCatalogEntries: mocks.resolveProviderInstallCatalogEntries,
}));
vi.mock(import("../../../plugins/official-external-plugin-catalog.js"), async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
getOfficialExternalPluginCatalogEntry: mocks.getOfficialExternalPluginCatalogEntry,
};
});
vi.mock("./missing-configured-plugin-install.js", () => ({
repairMissingPluginInstallsForIds: mocks.repairMissingPluginInstallsForIds,
}));
@@ -22,6 +31,7 @@ describe("configured plugin install release step", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.detectPluginAutoEnableCandidates.mockReturnValue([]);
mocks.getOfficialExternalPluginCatalogEntry.mockReturnValue(undefined);
mocks.resolveProviderInstallCatalogEntries.mockReturnValue([]);
mocks.repairMissingPluginInstallsForIds.mockResolvedValue({
changes: [],
@@ -372,4 +382,53 @@ describe("configured plugin install release step", () => {
touchedConfig: false,
});
});
it("includes allow-only official plugin ids in the repair set", async () => {
mocks.getOfficialExternalPluginCatalogEntry.mockImplementation((pluginId: string) => {
if (pluginId === "lobster") {
return { name: "@openclaw/lobster" };
}
return undefined;
});
const { collectReleaseConfiguredPluginIds } =
await import("./release-configured-plugin-installs.js");
const result = collectReleaseConfiguredPluginIds({
cfg: {
plugins: {
allow: ["lobster", "unofficial-custom"],
},
},
env: {},
});
expect(result.pluginIds).toEqual(["lobster"]);
expect(result.channelIds).toEqual([]);
});
it("skips allow-only plugin ids that already have material plugin entries", async () => {
mocks.getOfficialExternalPluginCatalogEntry.mockImplementation((pluginId: string) => {
if (pluginId === "lobster") {
return { name: "@openclaw/lobster" };
}
return undefined;
});
const { collectReleaseConfiguredPluginIds } =
await import("./release-configured-plugin-installs.js");
const result = collectReleaseConfiguredPluginIds({
cfg: {
plugins: {
allow: ["lobster"],
entries: {
lobster: { enabled: true },
},
},
},
env: {},
});
expect(result.pluginIds).toEqual(["lobster"]);
expect(mocks.getOfficialExternalPluginCatalogEntry).not.toHaveBeenCalledWith("lobster");
});
});

View File

@@ -7,6 +7,7 @@ import { detectPluginAutoEnableCandidates } from "../../../config/plugin-auto-en
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import { compareOpenClawVersions } from "../../../config/version.js";
import { isTruthyEnvValue } from "../../../infra/env.js";
import { getOfficialExternalPluginCatalogEntry } from "../../../plugins/official-external-plugin-catalog.js";
import { resolveProviderInstallCatalogEntries } from "../../../plugins/provider-install-catalog.js";
import { resolveWebSearchInstallCatalogEntry } from "../../../plugins/web-search-install-catalog.js";
import { VERSION } from "../../../version.js";
@@ -214,6 +215,27 @@ function collectAcpRuntimePluginIds(cfg: OpenClawConfig): string[] {
return ["acpx"];
}
function collectAllowOnlyOfficialPluginIds(cfg: OpenClawConfig): string[] {
const allow = cfg.plugins?.allow;
if (!Array.isArray(allow) || allow.length === 0) {
return [];
}
const materialEntryIds = new Set(
collectMaterialPluginEntryIds(cfg).map((id) => id.toLowerCase()),
);
const ids: string[] = [];
for (const rawPluginId of allow) {
const pluginId = normalizeId(rawPluginId);
if (!pluginId || materialEntryIds.has(pluginId.toLowerCase())) {
continue;
}
if (getOfficialExternalPluginCatalogEntry(pluginId)) {
ids.push(pluginId);
}
}
return ids;
}
function addEligiblePluginId(cfg: OpenClawConfig, pluginIds: Set<string>, pluginId: string): void {
const normalized = pluginId.trim();
if (!normalized || isDenied(cfg, normalized) || isDisabled(cfg, normalized)) {
@@ -274,6 +296,9 @@ export function collectReleaseConfiguredPluginIds(params: {
for (const pluginId of collectAcpRuntimePluginIds(params.cfg)) {
addEligiblePluginId(params.cfg, pluginIds, pluginId);
}
for (const pluginId of collectAllowOnlyOfficialPluginIds(params.cfg)) {
addEligiblePluginId(params.cfg, pluginIds, pluginId);
}
for (const channelId of collectConfiguredChannelIds(params.cfg, env)) {
if (
!isChannelDisabled(params.cfg, channelId) &&