fix(plugins): resolve official plugin install aliases

Resolve bare official external plugin IDs through the official catalog before generic npm fallback, preserving explicit npm semantics and catalog integrity through the hook-pack fallback.\n\nFixes #76373.\n\nThanks @bek91 and @vincentkoc.
This commit is contained in:
Bek
2026-05-03 01:27:13 -04:00
committed by GitHub
parent f696be950b
commit 411df59916
6 changed files with 262 additions and 1 deletions

View File

@@ -8,6 +8,7 @@ import {
applyExclusiveSlotSelection,
buildPluginSnapshotReport,
enablePluginInConfig,
findBundledPluginSourceMock,
installHooksFromNpmSpec,
installHooksFromPath,
installPluginFromClawHub,
@@ -652,7 +653,91 @@ describe("plugins cli install", () => {
});
});
it("installs bare plugin specs through npm without ClawHub lookup", async () => {
it("resolves exact official external plugin ids through their npm package", async () => {
const cfg = createEmptyPluginConfig();
const enabledCfg = createEnabledPluginConfig("brave");
loadConfig.mockReturnValue(cfg);
findBundledPluginSourceMock.mockReturnValue(undefined);
installPluginFromNpmSpec.mockResolvedValue(createNpmPluginInstallResult("brave"));
enablePluginInConfig.mockReturnValue({ config: enabledCfg });
applyExclusiveSlotSelection.mockReturnValue({
config: enabledCfg,
warnings: [],
});
await runPluginsCommand(["plugins", "install", "brave"]);
expect(findBundledPluginSourceMock).toHaveBeenCalledWith({
lookup: { kind: "pluginId", value: "brave" },
});
expect(installPluginFromClawHub).not.toHaveBeenCalled();
expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
expect.objectContaining({
spec: "@openclaw/brave-plugin",
expectedPluginId: "brave",
}),
);
expect(writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith({
brave: expect.objectContaining({
source: "npm",
spec: "@openclaw/brave-plugin",
installPath: cliInstallPath("brave"),
version: "1.2.3",
}),
});
expect(writeConfigFile).toHaveBeenCalledWith(enabledCfg);
});
it("passes official external catalog integrity to npm installs", async () => {
const cfg = createEmptyPluginConfig();
const enabledCfg = createEnabledPluginConfig("wecom");
loadConfig.mockReturnValue(cfg);
findBundledPluginSourceMock.mockReturnValue(undefined);
installPluginFromNpmSpec.mockResolvedValue(createNpmPluginInstallResult("wecom"));
enablePluginInConfig.mockReturnValue({ config: enabledCfg });
applyExclusiveSlotSelection.mockReturnValue({
config: enabledCfg,
warnings: [],
});
await runPluginsCommand(["plugins", "install", "wecom"]);
expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
expect.objectContaining({
spec: "@wecom/wecom-openclaw-plugin@2026.4.23",
expectedPluginId: "wecom",
expectedIntegrity:
"sha512-bnzfdIEEu1/LFvcdyjaTkyxt27w6c7dqhkPezU62OWaqmcdFsUGR3T55USK/O9pIKsNcnL1Tnu1pqKYCWHFgWQ==",
}),
);
});
it("passes official external catalog integrity to hook-pack fallback", async () => {
loadConfig.mockReturnValue(createEmptyPluginConfig());
findBundledPluginSourceMock.mockReturnValue(undefined);
installPluginFromNpmSpec.mockResolvedValue({
ok: false,
error: "package.json missing openclaw.extensions",
code: "missing_openclaw_extensions",
});
installHooksFromNpmSpec.mockResolvedValue({
ok: false,
error:
"aborted: npm package integrity drift detected for @wecom/wecom-openclaw-plugin@2026.4.23",
});
await expect(runPluginsCommand(["plugins", "install", "wecom"])).rejects.toThrow("__exit__:1");
expect(installHooksFromNpmSpec).toHaveBeenCalledWith(
expect.objectContaining({
spec: "@wecom/wecom-openclaw-plugin@2026.4.23",
expectedIntegrity:
"sha512-bnzfdIEEu1/LFvcdyjaTkyxt27w6c7dqhkPezU62OWaqmcdFsUGR3T55USK/O9pIKsNcnL1Tnu1pqKYCWHFgWQ==",
}),
);
});
it("installs ordinary bare plugin specs through npm without ClawHub lookup", async () => {
const cfg = createEmptyPluginConfig();
const enabledCfg = createEnabledPluginConfig("demo");
loadConfig.mockReturnValue(cfg);
@@ -735,6 +820,34 @@ describe("plugins cli install", () => {
expect(writeConfigFile).toHaveBeenCalledWith(enabledCfg);
});
it("keeps npm-prefixed official plugin ids on explicit npm semantics", async () => {
const cfg = createEmptyPluginConfig();
const enabledCfg = createEnabledPluginConfig("brave");
loadConfig.mockReturnValue(cfg);
installPluginFromNpmSpec.mockResolvedValue(createNpmPluginInstallResult("brave"));
enablePluginInConfig.mockReturnValue({ config: enabledCfg });
recordPluginInstall.mockReturnValue(enabledCfg);
applyExclusiveSlotSelection.mockReturnValue({
config: enabledCfg,
warnings: [],
});
await runPluginsCommand(["plugins", "install", "npm:brave"]);
expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
expect.objectContaining({
spec: "brave",
}),
);
expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
expect.not.objectContaining({
expectedPluginId: "brave",
}),
);
expect(installPluginFromClawHub).not.toHaveBeenCalled();
});
it("passes the active profile extensions dir to npm installs", async () => {
const extensionsDir = useProfileExtensionsDir();
const cfg = createEmptyPluginConfig();