diff --git a/src/plugins/update.test.ts b/src/plugins/update.test.ts index 6219376a37b..07e1dc35969 100644 --- a/src/plugins/update.test.ts +++ b/src/plugins/update.test.ts @@ -15,6 +15,76 @@ describe("updateNpmInstalledPlugins", () => { installPluginFromNpmSpecMock.mockReset(); }); + it("skips integrity drift checks for unpinned npm specs during dry-run updates", async () => { + installPluginFromNpmSpecMock.mockResolvedValue({ + ok: true, + pluginId: "opik-openclaw", + targetDir: "/tmp/opik-openclaw", + version: "0.2.6", + extensions: ["index.ts"], + }); + + const { updateNpmInstalledPlugins } = await import("./update.js"); + await updateNpmInstalledPlugins({ + config: { + plugins: { + installs: { + "opik-openclaw": { + source: "npm", + spec: "@opik/opik-openclaw", + integrity: "sha512-old", + installPath: "/tmp/opik-openclaw", + }, + }, + }, + }, + pluginIds: ["opik-openclaw"], + dryRun: true, + }); + + expect(installPluginFromNpmSpecMock).toHaveBeenCalledWith( + expect.objectContaining({ + spec: "@opik/opik-openclaw", + expectedIntegrity: undefined, + }), + ); + }); + + it("keeps integrity drift checks for exact-version npm specs during dry-run updates", async () => { + installPluginFromNpmSpecMock.mockResolvedValue({ + ok: true, + pluginId: "opik-openclaw", + targetDir: "/tmp/opik-openclaw", + version: "0.2.6", + extensions: ["index.ts"], + }); + + const { updateNpmInstalledPlugins } = await import("./update.js"); + await updateNpmInstalledPlugins({ + config: { + plugins: { + installs: { + "opik-openclaw": { + source: "npm", + spec: "@opik/opik-openclaw@0.2.5", + integrity: "sha512-old", + installPath: "/tmp/opik-openclaw", + }, + }, + }, + }, + pluginIds: ["opik-openclaw"], + dryRun: true, + }); + + expect(installPluginFromNpmSpecMock).toHaveBeenCalledWith( + expect.objectContaining({ + spec: "@opik/opik-openclaw@0.2.5", + expectedIntegrity: "sha512-old", + }), + ); + }); + it("formats package-not-found updates with a stable message", async () => { installPluginFromNpmSpecMock.mockResolvedValue({ ok: false, diff --git a/src/plugins/update.ts b/src/plugins/update.ts index 622d0e97616..553867425a9 100644 --- a/src/plugins/update.ts +++ b/src/plugins/update.ts @@ -80,6 +80,28 @@ type InstallIntegrityDrift = { }; }; +function expectedIntegrityForUpdate( + spec: string | undefined, + integrity: string | undefined, +): string | undefined { + if (!integrity || !spec) { + return undefined; + } + const value = spec.trim(); + if (!value) { + return undefined; + } + const at = value.lastIndexOf("@"); + if (at <= 0 || at >= value.length - 1) { + return undefined; + } + const version = value.slice(at + 1).trim(); + if (!/^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version)) { + return undefined; + } + return integrity; +} + async function readInstalledPackageVersion(dir: string): Promise { const manifestPath = path.join(dir, "package.json"); const opened = openBoundaryFileSync({ @@ -246,7 +268,7 @@ export async function updateNpmInstalledPlugins(params: { mode: "update", dryRun: true, expectedPluginId: pluginId, - expectedIntegrity: record.integrity, + expectedIntegrity: expectedIntegrityForUpdate(record.spec, record.integrity), onIntegrityDrift: createPluginUpdateIntegrityDriftHandler({ pluginId, dryRun: true, @@ -305,7 +327,7 @@ export async function updateNpmInstalledPlugins(params: { spec: record.spec, mode: "update", expectedPluginId: pluginId, - expectedIntegrity: record.integrity, + expectedIntegrity: expectedIntegrityForUpdate(record.spec, record.integrity), onIntegrityDrift: createPluginUpdateIntegrityDriftHandler({ pluginId, dryRun: false,