diff --git a/server/src/__tests__/bundled-plugins.test.ts b/server/src/__tests__/bundled-plugins.test.ts index ac68686258..d4369ebb8b 100644 --- a/server/src/__tests__/bundled-plugins.test.ts +++ b/server/src/__tests__/bundled-plugins.test.ts @@ -498,6 +498,17 @@ describe("ensureBundledPlugins", () => { expect(deps.lifecycle.load).not.toHaveBeenCalled(); }); + it("ignores object key order when comparing a distribution manifest read from JSONB", async () => { + const localPath = path.join(CATALOG_ROOT, "distribution/widget"); + const distribution = { key: "widget", pluginKey: "acme.widget", version: "0.1.0", directory: "widget", digest: `sha256:${"a".repeat(64)}`, localPath, entrypoints: { worker: "dist/worker.js" } }; + const manifest = { ...makeManifest("acme.widget", "0.1.0"), entrypoints: { worker: "dist/worker.js", ui: "dist/ui" } }; + const reordered = { entrypoints: { ui: "dist/ui", worker: "dist/worker.js" }, capabilities: [], version: "0.1.0", apiVersion: 1, id: "acme.widget" }; + const { deps, loadManifest, update } = makeDeps({ rows: { "acme.widget": { id: "row-widget", pluginKey: "acme.widget", status: "ready", packagePath: localPath, manifestJson: reordered } } }); + loadManifest.mockResolvedValue(manifest); + await ensureBundledPlugins([{ ...distribution, distribution }], deps, { reinstallUninstalled: true }); + expect(update).not.toHaveBeenCalled(); + }); + it("swallows a reconcile error and continues boot", async () => { const { deps, update } = makeDeps({ rows: { diff --git a/server/src/services/bundled-plugins.ts b/server/src/services/bundled-plugins.ts index d3ce107996..d4e859259d 100644 --- a/server/src/services/bundled-plugins.ts +++ b/server/src/services/bundled-plugins.ts @@ -1,5 +1,6 @@ import path from "node:path"; import fs from "node:fs"; +import { isDeepStrictEqual } from "node:util"; import type { PaperclipPluginManifestV1 } from "@paperclipai/shared"; import { assertDistributionManifestCapabilities, readDistributionPluginCatalog, type DistributionPlugin } from "./distribution-plugin-catalog.js"; @@ -278,7 +279,7 @@ async function reconcileBundledPluginManifest( requiresApproval = bundleManifest.capabilities.some((capability) => !approved.has(capability)); } const rebindPackage = install.distribution && existing.packagePath !== install.localPath && existing.status !== "uninstalled"; - const refreshDistribution = install.distribution && JSON.stringify(bundleManifest) !== JSON.stringify(existing.manifestJson); + const refreshDistribution = install.distribution && !isDeepStrictEqual(bundleManifest, existing.manifestJson); if (bundleManifest.version === existing.version && !rebindPackage && !requiresApproval && !refreshDistribution) return; await deps.registry.update(existing.id, { version: bundleManifest.version,