diff --git a/server/src/__tests__/bundled-plugins.test.ts b/server/src/__tests__/bundled-plugins.test.ts index fef938c3a7..26be20b4b4 100644 --- a/server/src/__tests__/bundled-plugins.test.ts +++ b/server/src/__tests__/bundled-plugins.test.ts @@ -472,6 +472,17 @@ describe("ensureBundledPlugins", () => { expect(deps.logger.error).toHaveBeenCalled(); }); + it("does not replace an operator uninstall with a distribution approval request", 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 { deps, loadManifest, update, updateStatus, installPlugin } = makeDeps({ rows: { "acme.widget": { id: "row-widget", pluginKey: "acme.widget", status: "uninstalled" } } }); + loadManifest.mockResolvedValue({ ...makeManifest("acme.widget", "0.1.0"), capabilities: ["issues.read"] }); + await ensureBundledPlugins([{ ...distribution, distribution }], deps, { reinstallUninstalled: false }); + expect(update).not.toHaveBeenCalled(); + expect(updateStatus).not.toHaveBeenCalled(); + expect(installPlugin).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 be321ce9a2..0a53ced421 100644 --- a/server/src/services/bundled-plugins.ts +++ b/server/src/services/bundled-plugins.ts @@ -265,6 +265,9 @@ async function reconcileBundledPluginManifest( verifiedManifest?: PaperclipPluginManifestV1, ): Promise<"upgrade_pending" | undefined> { try { + // Managed reinstalls take the install path instead; this branch means the + // operator's uninstall must be retained, including its status. + if (install.distribution && existing.status === "uninstalled") return; if (!verifiedManifest && !bundleManifestExists(install.localPath)) return; const bundleManifest = verifiedManifest ?? await deps.loader.loadManifest(install.localPath); if (!bundleManifest) return;