diff --git a/extensions/matrix/package.json b/extensions/matrix/package.json index e15189ce51e..ddb42d2864d 100644 --- a/extensions/matrix/package.json +++ b/extensions/matrix/package.json @@ -30,6 +30,9 @@ "./index.ts" ], "setupEntry": "./setup-entry.ts", + "setupFeatures": { + "configPromotion": true + }, "channel": { "id": "matrix", "label": "Matrix", diff --git a/extensions/telegram/package.json b/extensions/telegram/package.json index 0f20e2afeae..26644cdafe6 100644 --- a/extensions/telegram/package.json +++ b/extensions/telegram/package.json @@ -20,6 +20,7 @@ ], "setupEntry": "./setup-entry.ts", "setupFeatures": { + "configPromotion": true, "legacyStateMigrations": true }, "channel": { diff --git a/src/channels/plugins/bundled.ts b/src/channels/plugins/bundled.ts index d2e76dce974..50ed68e094a 100644 --- a/src/channels/plugins/bundled.ts +++ b/src/channels/plugins/bundled.ts @@ -50,6 +50,11 @@ type BundledChannelSetupEntryRuntimeContract = { }; }; +type BundledChannelPackageSetupFeature = + | "configPromotion" + | "legacyStateMigrations" + | "legacySessionSurfaces"; + type GeneratedBundledChannelEntry = { id: string; entry: BundledChannelEntryRuntimeContract; @@ -507,6 +512,16 @@ export function listBundledChannelPluginIds(): readonly ChannelId[] { return listBundledChannelPluginIdsForRoot(resolveBundledChannelRootScope()); } +export function hasBundledChannelPackageSetupFeature( + id: ChannelId, + feature: BundledChannelPackageSetupFeature, +): boolean { + const rootScope = resolveBundledChannelRootScope(); + return ( + resolveBundledChannelMetadata(id, rootScope)?.packageManifest?.setupFeatures?.[feature] === true + ); +} + function resolveBundledChannelMetadata( id: ChannelId, rootScope: BundledChannelRootScope, diff --git a/src/channels/plugins/setup-promotion-helpers.test.ts b/src/channels/plugins/setup-promotion-helpers.test.ts index 048442932a7..c9fb560fef8 100644 --- a/src/channels/plugins/setup-promotion-helpers.test.ts +++ b/src/channels/plugins/setup-promotion-helpers.test.ts @@ -1,10 +1,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const getBundledChannelPluginMock = vi.hoisted(() => vi.fn()); +const hasBundledChannelPackageSetupFeatureMock = vi.hoisted(() => vi.fn()); const getLoadedChannelPluginMock = vi.hoisted(() => vi.fn()); vi.mock("./bundled.js", () => ({ getBundledChannelPlugin: getBundledChannelPluginMock, + hasBundledChannelPackageSetupFeature: hasBundledChannelPackageSetupFeatureMock, })); vi.mock("./registry.js", () => ({ @@ -19,6 +21,8 @@ import { describe("setup promotion helpers", () => { beforeEach(() => { getBundledChannelPluginMock.mockReset(); + hasBundledChannelPackageSetupFeatureMock.mockReset(); + hasBundledChannelPackageSetupFeatureMock.mockReturnValue(false); getLoadedChannelPluginMock.mockReset(); }); @@ -38,9 +42,9 @@ describe("setup promotion helpers", () => { expect(getBundledChannelPluginMock).not.toHaveBeenCalled(); }); - it("keeps WhatsApp static promotion cheap even when named accounts already exist", () => { + it("skips bundled setup promotion without a manifest feature", () => { const keys = resolveSingleAccountKeysToMove({ - channelKey: "whatsapp", + channelKey: "demo", channel: { accounts: { work: { enabled: true }, @@ -53,11 +57,16 @@ describe("setup promotion helpers", () => { }); expect(keys).toEqual(["dmPolicy", "allowFrom", "groupPolicy", "groupAllowFrom"]); - expect(getLoadedChannelPluginMock).toHaveBeenCalledWith("whatsapp"); + expect(getLoadedChannelPluginMock).toHaveBeenCalledWith("demo"); + expect(hasBundledChannelPackageSetupFeatureMock).toHaveBeenCalledWith( + "demo", + "configPromotion", + ); expect(getBundledChannelPluginMock).not.toHaveBeenCalled(); }); it("loads bundled setup only for non-static migration keys", () => { + hasBundledChannelPackageSetupFeatureMock.mockReturnValue(true); getBundledChannelPluginMock.mockReturnValue({ setup: { singleAccountKeysToMove: ["customAuth"], @@ -96,6 +105,7 @@ describe("setup promotion helpers", () => { }); it("loads bundled setup for named-account filters before registry bootstrap", () => { + hasBundledChannelPackageSetupFeatureMock.mockReturnValue(true); getBundledChannelPluginMock.mockReturnValue({ setup: { namedAccountPromotionKeys: ["token"], diff --git a/src/channels/plugins/setup-promotion-helpers.ts b/src/channels/plugins/setup-promotion-helpers.ts index b38fd21261f..640c16befcc 100644 --- a/src/channels/plugins/setup-promotion-helpers.ts +++ b/src/channels/plugins/setup-promotion-helpers.ts @@ -1,6 +1,6 @@ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; -import { getBundledChannelPlugin } from "./bundled.js"; +import { getBundledChannelPlugin, hasBundledChannelPackageSetupFeature } from "./bundled.js"; import { getLoadedChannelPlugin } from "./registry.js"; type ChannelSectionBase = { @@ -49,8 +49,6 @@ type ChannelSetupPromotionSurface = { }) => string | undefined; }; -const BUNDLED_CHANNELS_WITHOUT_SETUP_PROMOTION_SURFACE = new Set(["whatsapp"]); - function asPromotionSurface(setup: unknown): ChannelSetupPromotionSurface | null { return setup && typeof setup === "object" ? (setup as ChannelSetupPromotionSurface) : null; } @@ -64,7 +62,7 @@ function getLoadedChannelSetupPromotionSurface( function getBundledChannelSetupPromotionSurface( channelKey: string, ): ChannelSetupPromotionSurface | null { - if (BUNDLED_CHANNELS_WITHOUT_SETUP_PROMOTION_SURFACE.has(channelKey)) { + if (!hasBundledChannelPackageSetupFeature(channelKey, "configPromotion")) { return null; } return asPromotionSurface(getBundledChannelPlugin(channelKey)?.setup); diff --git a/src/plugins/manifest.ts b/src/plugins/manifest.ts index 91e131a1585..fe5931f7b03 100644 --- a/src/plugins/manifest.ts +++ b/src/plugins/manifest.ts @@ -975,6 +975,7 @@ export type OpenClawPackageStartup = { }; export type OpenClawPackageSetupFeatures = { + configPromotion?: boolean; legacyStateMigrations?: boolean; legacySessionSurfaces?: boolean; };