fix: align official plugin repair versions

This commit is contained in:
Peter Steinberger
2026-05-04 00:18:40 +01:00
parent 275e2f92bd
commit f0bb00f54b
2 changed files with 50 additions and 24 deletions

View File

@@ -1,4 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { VERSION } from "../../../version.js";
const openClawReleaseSpec = (packageName: string) => `${packageName}@${VERSION}`;
const mocks = vi.hoisted(() => ({
installPluginFromClawHub: vi.fn(),
@@ -343,13 +346,13 @@ describe("repairMissingConfiguredPluginInstalls", () => {
expect(mocks.installPluginFromClawHub).not.toHaveBeenCalled();
expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith(
expect.objectContaining({
spec: "@openclaw/twitch",
spec: openClawReleaseSpec("@openclaw/twitch"),
expectedPluginId: "twitch",
trustedSourceLinkedOfficialInstall: true,
}),
);
expect(result.changes).toEqual([
'Installed missing configured plugin "twitch" from @openclaw/twitch.',
`Installed missing configured plugin "twitch" from ${openClawReleaseSpec("@openclaw/twitch")}.`,
]);
});
@@ -395,12 +398,12 @@ describe("repairMissingConfiguredPluginInstalls", () => {
expect(mocks.installPluginFromClawHub).not.toHaveBeenCalled();
expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith(
expect.objectContaining({
spec: "@openclaw/diagnostics-otel",
spec: openClawReleaseSpec("@openclaw/diagnostics-otel"),
expectedPluginId: "diagnostics-otel",
}),
);
expect(result.changes).toEqual([
'Installed missing configured plugin "diagnostics-otel" from @openclaw/diagnostics-otel.',
`Installed missing configured plugin "diagnostics-otel" from ${openClawReleaseSpec("@openclaw/diagnostics-otel")}.`,
]);
});
@@ -442,13 +445,13 @@ describe("repairMissingConfiguredPluginInstalls", () => {
expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith(
expect.objectContaining({
spec: "@openclaw/acpx",
spec: openClawReleaseSpec("@openclaw/acpx"),
expectedPluginId: "acpx",
trustedSourceLinkedOfficialInstall: true,
}),
);
expect(result.changes).toEqual([
'Installed missing configured plugin "acpx" from @openclaw/acpx.',
`Installed missing configured plugin "acpx" from ${openClawReleaseSpec("@openclaw/acpx")}.`,
]);
});
@@ -999,7 +1002,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
expect(mocks.resolveProviderInstallCatalogEntries).toHaveBeenCalled();
expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith(
expect.objectContaining({
spec: "@openclaw/codex",
spec: openClawReleaseSpec("@openclaw/codex"),
expectedPluginId: "codex",
trustedSourceLinkedOfficialInstall: true,
}),
@@ -1008,7 +1011,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
expect.objectContaining({
codex: expect.objectContaining({
source: "npm",
spec: "@openclaw/codex",
spec: openClawReleaseSpec("@openclaw/codex"),
installPath: "/tmp/openclaw-plugins/codex",
version: "2026.5.2",
}),
@@ -1016,7 +1019,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
{ env: {} },
);
expect(result.changes).toEqual([
'Installed missing configured plugin "codex" from @openclaw/codex.',
`Installed missing configured plugin "codex" from ${openClawReleaseSpec("@openclaw/codex")}.`,
]);
expect(result.warnings).toEqual([]);
});
@@ -1077,7 +1080,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith(
expect.objectContaining({
spec: "@openclaw/codex",
spec: openClawReleaseSpec("@openclaw/codex"),
expectedPluginId: "codex",
trustedSourceLinkedOfficialInstall: true,
}),
@@ -1086,7 +1089,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
expect.objectContaining({
codex: expect.objectContaining({
source: "npm",
spec: "@openclaw/codex",
spec: openClawReleaseSpec("@openclaw/codex"),
installPath: "/tmp/openclaw-plugins/codex",
version: "2026.5.2",
}),
@@ -1094,7 +1097,9 @@ describe("repairMissingConfiguredPluginInstalls", () => {
{ env },
);
expect(result).toEqual({
changes: ['Installed missing configured plugin "codex" from @openclaw/codex.'],
changes: [
`Installed missing configured plugin "codex" from ${openClawReleaseSpec("@openclaw/codex")}.`,
],
warnings: [],
});
});
@@ -1271,7 +1276,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
);
expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith(
expect.objectContaining({
spec: "@openclaw/discord",
spec: openClawReleaseSpec("@openclaw/discord"),
expectedPluginId: "discord",
trustedSourceLinkedOfficialInstall: true,
}),
@@ -1283,7 +1288,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
{ env: {} },
);
expect(result.changes).toEqual([
'Installed missing configured plugin "discord" from @openclaw/discord.',
`Installed missing configured plugin "discord" from ${openClawReleaseSpec("@openclaw/discord")}.`,
]);
});
@@ -1616,13 +1621,13 @@ describe("repairMissingConfiguredPluginInstalls", () => {
expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith(
expect.objectContaining({
spec: "@openclaw/brave-plugin",
spec: openClawReleaseSpec("@openclaw/brave-plugin"),
expectedPluginId: "brave",
trustedSourceLinkedOfficialInstall: true,
}),
);
expect(result.changes).toEqual([
'Installed missing configured plugin "brave" from @openclaw/brave-plugin.',
`Installed missing configured plugin "brave" from ${openClawReleaseSpec("@openclaw/brave-plugin")}.`,
]);
});

View File

@@ -8,7 +8,7 @@ import { listChannelPluginCatalogEntries } from "../../../channels/plugins/catal
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import type { PluginInstallRecord } from "../../../config/types.plugins.js";
import { parseClawHubPluginSpec } from "../../../infra/clawhub-spec.js";
import { parseRegistryNpmSpec } from "../../../infra/npm-registry-spec.js";
import { isExactSemverVersion, parseRegistryNpmSpec } from "../../../infra/npm-registry-spec.js";
import { buildClawHubPluginInstallRecordFields } from "../../../plugins/clawhub-install-records.js";
import { CLAWHUB_INSTALL_ERROR_CODE, installPluginFromClawHub } from "../../../plugins/clawhub.js";
import { resolveDefaultPluginExtensionsDir } from "../../../plugins/install-paths.js";
@@ -30,6 +30,7 @@ import { resolveProviderInstallCatalogEntries } from "../../../plugins/provider-
import { updateNpmInstalledPlugins } from "../../../plugins/update.js";
import { resolveWebSearchInstallCatalogEntry } from "../../../plugins/web-search-install-catalog.js";
import { resolveUserPath } from "../../../utils.js";
import { VERSION } from "../../../version.js";
import { asObjectRecord } from "./object.js";
type DownloadableInstallCandidate = {
@@ -381,6 +382,27 @@ function recordClawHubPackageName(value: string | undefined): string | undefined
return parseClawHubPluginSpec(trimmed)?.name ?? trimmed;
}
function resolveVersionAlignedOfficialNpmSpec(
candidate: DownloadableInstallCandidate,
): string | undefined {
const npmSpec = candidate.npmSpec?.trim();
if (!npmSpec || !candidate.trustedSourceLinkedOfficialInstall) {
return npmSpec;
}
const parsed = parseRegistryNpmSpec(npmSpec);
if (!parsed || !parsed.name.startsWith("@openclaw/")) {
return npmSpec;
}
if (parsed.selectorKind === "exact-version") {
return npmSpec;
}
const hostVersion = VERSION.trim();
if (!isExactSemverVersion(hostVersion)) {
return npmSpec;
}
return `${parsed.name}@${hostVersion}`;
}
async function installCandidate(params: {
candidate: DownloadableInstallCandidate;
records: Record<string, PluginInstallRecord>;
@@ -430,7 +452,8 @@ async function installCandidate(params: {
`ClawHub ${candidate.clawhubSpec} unavailable for "${candidate.pluginId}"; falling back to npm ${candidate.npmSpec}.`,
);
}
if (!candidate.npmSpec) {
const npmSpec = resolveVersionAlignedOfficialNpmSpec(candidate);
if (!npmSpec) {
return {
records: params.records,
changes: [],
@@ -440,7 +463,7 @@ async function installCandidate(params: {
};
}
const result = await installPluginFromNpmSpec({
spec: candidate.npmSpec,
spec: npmSpec,
extensionsDir,
expectedPluginId: candidate.pluginId,
expectedIntegrity: candidate.expectedIntegrity,
@@ -464,17 +487,14 @@ async function installCandidate(params: {
...params.records,
[pluginId]: {
source: "npm",
spec: candidate.npmSpec,
spec: npmSpec,
installPath: result.targetDir,
version: result.version,
installedAt: new Date().toISOString(),
...buildNpmResolutionInstallFields(result.npmResolution),
},
},
changes: [
...changes,
`Installed missing configured plugin "${pluginId}" from ${candidate.npmSpec}.`,
],
changes: [...changes, `Installed missing configured plugin "${pluginId}" from ${npmSpec}.`],
warnings: [],
};
}
@@ -672,4 +692,5 @@ export const __testing = {
collectConfiguredChannelIds,
collectConfiguredPluginIds,
collectDownloadableInstallCandidates,
resolveVersionAlignedOfficialNpmSpec,
};