chore(channels): remove bluebubbles bundled surface

This commit is contained in:
Vincent Koc
2026-05-07 04:23:00 -07:00
parent f482e4d335
commit c97998ce21
76 changed files with 200 additions and 580 deletions

View File

@@ -14,7 +14,6 @@ query-filters:
- security
paths:
- extensions/bluebubbles/src
- extensions/discord/src
- extensions/feishu/src
- extensions/googlechat/src

5
.github/labeler.yml vendored
View File

@@ -1,8 +1,3 @@
"channel: bluebubbles":
- changed-files:
- any-glob-to-any-file:
- "extensions/bluebubbles/**"
- "docs/channels/bluebubbles.md"
"plugin: azure-speech":
- changed-files:
- any-glob-to-any-file:

View File

@@ -36,7 +36,6 @@ on:
- "src/*.ts"
- "src/**/*.ts"
- "src/config/**"
- "extensions/bluebubbles/src/**"
- "extensions/discord/src/**"
- "extensions/feishu/src/**"
- "extensions/googlechat/src/**"
@@ -229,7 +228,7 @@ jobs:
src/auto-reply/reply/post-compaction-context.ts|src/auto-reply/reply/queue/*|src/auto-reply/reply/startup-context.ts|src/commands/doctor-session-*.ts|src/commands/session-store-targets.ts|src/commands/sessions*.ts|src/infra/diagnostic-*.ts|src/infra/diagnostics-timeline.ts|src/infra/session-delivery-queue*.ts|src/logging/diagnostic*.ts)
session_diagnostics=true
;;
extensions/bluebubbles/src/*|extensions/discord/src/*|extensions/feishu/src/*|extensions/googlechat/src/*|extensions/imessage/src/*|extensions/irc/src/*|extensions/line/src/*|extensions/matrix/src/*|extensions/mattermost/src/*|extensions/msteams/src/*|extensions/nextcloud-talk/src/*|extensions/nostr/src/*|extensions/qa-channel/src/*|extensions/qqbot/src/*|extensions/signal/src/*|extensions/slack/src/*|extensions/synology-chat/src/*|extensions/telegram/src/*|extensions/tlon/src/*|extensions/twitch/src/*|extensions/whatsapp/src/*|extensions/zalo/src/*|extensions/zalouser/src/*|src/channels/*)
extensions/discord/src/*|extensions/feishu/src/*|extensions/googlechat/src/*|extensions/imessage/src/*|extensions/irc/src/*|extensions/line/src/*|extensions/matrix/src/*|extensions/mattermost/src/*|extensions/msteams/src/*|extensions/nextcloud-talk/src/*|extensions/nostr/src/*|extensions/qa-channel/src/*|extensions/qqbot/src/*|extensions/signal/src/*|extensions/slack/src/*|extensions/synology-chat/src/*|extensions/telegram/src/*|extensions/tlon/src/*|extensions/twitch/src/*|extensions/whatsapp/src/*|extensions/zalo/src/*|extensions/zalouser/src/*|src/channels/*)
channel=true
;;
src/config/*)

View File

@@ -144,7 +144,6 @@ uninstall, and publishing commands.
| Plugin | Description | Distribution | Surface |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
| [acpx](/plugins/reference/acpx) | Embedded ACP runtime backend with plugin-owned session and transport management. | `@openclaw/acpx`<br />npm; ClawHub | skills |
| [bluebubbles](/plugins/reference/bluebubbles) | Adds the BlueBubbles channel surface for sending and receiving OpenClaw messages. | `@openclaw/bluebubbles`<br />npm; ClawHub | channels: bluebubbles |
| [brave](/plugins/reference/brave) | Adds web search provider support. | `@openclaw/brave-plugin`<br />npm; ClawHub | contracts: webSearchProviders |
| [codex](/plugins/reference/codex) | Codex app-server harness and Codex-managed GPT model catalog. | `@openclaw/codex`<br />npm; ClawHub | providers: codex; contracts: mediaUnderstandingProviders, migrationProviders |
| [diagnostics-otel](/plugins/reference/diagnostics-otel) | OpenClaw diagnostics OpenTelemetry exporter. | `@openclaw/diagnostics-otel`<br />npm; ClawHub: `clawhub:@openclaw/diagnostics-otel` | plugin |

View File

@@ -25,7 +25,6 @@ pnpm plugins:inventory:gen
| [anthropic-vertex](/plugins/reference/anthropic-vertex) | Adds Anthropic Vertex model provider support to OpenClaw. | `@openclaw/anthropic-vertex-provider`<br />included in OpenClaw | providers: anthropic-vertex |
| [arcee](/plugins/reference/arcee) | Adds Arcee model provider support to OpenClaw. | `@openclaw/arcee-provider`<br />included in OpenClaw | providers: arcee |
| [azure-speech](/plugins/reference/azure-speech) | Azure AI Speech text-to-speech (MP3, native Ogg/Opus voice notes, PCM telephony). | `@openclaw/azure-speech`<br />included in OpenClaw | contracts: speechProviders |
| [bluebubbles](/plugins/reference/bluebubbles) | Adds the BlueBubbles channel surface for sending and receiving OpenClaw messages. | `@openclaw/bluebubbles`<br />npm; ClawHub | channels: bluebubbles |
| [bonjour](/plugins/reference/bonjour) | Advertise the local OpenClaw gateway over Bonjour/mDNS. | `@openclaw/bonjour`<br />included in OpenClaw | plugin |
| [brave](/plugins/reference/brave) | Adds web search provider support. | `@openclaw/brave-plugin`<br />npm; ClawHub | contracts: webSearchProviders |
| [browser](/plugins/reference/browser) | Adds agent-callable tools. | `@openclaw/browser-plugin`<br />included in OpenClaw | contracts: tools; skills |

View File

@@ -33,7 +33,6 @@
"!dist/extensions/acpx/**",
"!dist/extensions/node_modules/**",
"!dist/extensions/*/node_modules/**",
"!dist/extensions/bluebubbles/**",
"!dist/extensions/brave/**",
"!dist/extensions/codex/**",
"!dist/extensions/diagnostics-otel/**",

9
pnpm-lock.yaml generated
View File

@@ -397,15 +397,6 @@ importers:
specifier: workspace:*
version: link:../../packages/plugin-sdk
extensions/bluebubbles:
devDependencies:
'@openclaw/plugin-sdk':
specifier: workspace:*
version: link:../../packages/plugin-sdk
openclaw:
specifier: workspace:*
version: link:../..
extensions/bonjour:
dependencies:
'@homebridge/ciao':

View File

@@ -38,7 +38,6 @@ const systemMarkLiteralGuardSources = [
];
const channelIds = [
"bluebubbles",
"discord",
"googlechat",
"imessage",
@@ -103,7 +102,7 @@ function matchesChannelModuleSpecifier(specifier) {
}
const userFacingChannelNameRe =
/\b(?:discord|telegram|slack|signal|imessage|whatsapp|google\s*chat|irc|line|zalo|matrix|msteams|bluebubbles)\b/i;
/\b(?:discord|telegram|slack|signal|imessage|whatsapp|google\s*chat|irc|line|zalo|matrix|msteams)\b/i;
const systemMarkLiteral = "⚙️";
function isModuleSpecifierStringNode(node) {

View File

@@ -14,8 +14,6 @@ const sourceRoots = ["src/channels", "src/routing", "src/line", "extensions"];
// Temporary allowlist for legacy callsites. New raw fetch callsites in channel/plugin runtime
// code should be rejected and migrated to fetchWithSsrFGuard/shared channel helpers.
const allowedRawFetchCallsites = new Set([
bundledPluginCallsite("bluebubbles", "src/test-harness.ts", 132),
bundledPluginCallsite("bluebubbles", "src/types.ts", 204),
bundledPluginCallsite("browser", "src/browser/cdp.helpers.ts", 268),
bundledPluginCallsite("browser", "src/browser/client-fetch.ts", 192),
bundledPluginCallsite("chutes", "models.ts", 536),

View File

@@ -8,7 +8,6 @@ import { runAsScript, toLine, unwrapExpression } from "./lib/ts-guard-utils.mjs"
const sourceRoots = ["extensions"];
const enforcedFiles = new Set([
bundledPluginFile("bluebubbles", "src/monitor.ts"),
bundledPluginFile("feishu", "src/monitor.transport.ts"),
bundledPluginFile("googlechat", "src/monitor.ts"),
bundledPluginFile("zalo", "src/monitor.webhook.ts"),

View File

@@ -91,7 +91,6 @@ function humanizeId(value) {
["api", "API"],
["aws", "AWS"],
["azure", "Azure"],
["bluebubbles", "BlueBubbles"],
["byteplus", "BytePlus"],
["codex", "Codex"],
["cli", "CLI"],

View File

@@ -2,7 +2,6 @@ import fs from "node:fs";
import path from "node:path";
import { channelTestRoots } from "../../test/vitest/vitest.channel-paths.mjs";
import { isAcpxExtensionRoot } from "../../test/vitest/vitest.extension-acpx-paths.mjs";
import { isBlueBubblesExtensionRoot } from "../../test/vitest/vitest.extension-bluebubbles-paths.mjs";
import { isBrowserExtensionRoot } from "../../test/vitest/vitest.extension-browser-paths.mjs";
import { resolveSplitChannelExtensionShard } from "../../test/vitest/vitest.extension-channel-split-paths.mjs";
import { isDiffsExtensionRoot } from "../../test/vitest/vitest.extension-diffs-paths.mjs";
@@ -34,7 +33,6 @@ const EXTENSION_TEST_COST_MULTIPLIERS = {
// These ratios come from Blacksmith extension batch timings; import-heavy
// suites vary widely, and file count alone leaves long tail shards.
"test/vitest/vitest.extension-acpx.config.ts": 0.75,
"test/vitest/vitest.extension-bluebubbles.config.ts": 0.8,
"test/vitest/vitest.extension-browser.config.ts": 0.5,
"test/vitest/vitest.extension-diffs.config.ts": 0.6,
"test/vitest/vitest.extension-discord.config.ts": 0.62,
@@ -149,7 +147,6 @@ export function resolveExtensionTestPlan(params = {}) {
const usesAcpxConfig = roots.some((root) => isAcpxExtensionRoot(root));
const usesBrowserConfig = roots.some((root) => isBrowserExtensionRoot(root));
const usesDiffsConfig = roots.some((root) => isDiffsExtensionRoot(root));
const usesBlueBubblesConfig = roots.some((root) => isBlueBubblesExtensionRoot(root));
const usesFeishuConfig = roots.some((root) => isFeishuExtensionRoot(root));
const usesIrcConfig = roots.some((root) => isIrcExtensionRoot(root));
const usesMattermostConfig = roots.some((root) => isMattermostExtensionRoot(root));
@@ -172,45 +169,43 @@ export function resolveExtensionTestPlan(params = {}) {
? "test/vitest/vitest.extension-channels.config.ts"
: usesAcpxConfig
? "test/vitest/vitest.extension-acpx.config.ts"
: usesBlueBubblesConfig
? "test/vitest/vitest.extension-bluebubbles.config.ts"
: usesBrowserConfig
? "test/vitest/vitest.extension-browser.config.ts"
: usesDiffsConfig
? "test/vitest/vitest.extension-diffs.config.ts"
: usesFeishuConfig
? "test/vitest/vitest.extension-feishu.config.ts"
: usesIrcConfig
? "test/vitest/vitest.extension-irc.config.ts"
: usesMattermostConfig
? "test/vitest/vitest.extension-mattermost.config.ts"
: usesMatrixConfig
? "test/vitest/vitest.extension-matrix.config.ts"
: usesMediaConfig
? "test/vitest/vitest.extension-media.config.ts"
: usesMemoryConfig
? "test/vitest/vitest.extension-memory.config.ts"
: usesMessagingConfig
? "test/vitest/vitest.extension-messaging.config.ts"
: usesMiscConfig
? "test/vitest/vitest.extension-misc.config.ts"
: usesMsTeamsConfig
? "test/vitest/vitest.extension-msteams.config.ts"
: usesQaConfig
? "test/vitest/vitest.extension-qa.config.ts"
: usesTelegramConfig
? "test/vitest/vitest.extension-telegram.config.ts"
: usesVoiceCallConfig
? "test/vitest/vitest.extension-voice-call.config.ts"
: usesWhatsAppConfig
? "test/vitest/vitest.extension-whatsapp.config.ts"
: usesZaloConfig
? "test/vitest/vitest.extension-zalo.config.ts"
: usesProviderOpenAiConfig
? "test/vitest/vitest.extension-provider-openai.config.ts"
: usesProviderConfig
? "test/vitest/vitest.extension-providers.config.ts"
: "test/vitest/vitest.extensions.config.ts";
: usesBrowserConfig
? "test/vitest/vitest.extension-browser.config.ts"
: usesDiffsConfig
? "test/vitest/vitest.extension-diffs.config.ts"
: usesFeishuConfig
? "test/vitest/vitest.extension-feishu.config.ts"
: usesIrcConfig
? "test/vitest/vitest.extension-irc.config.ts"
: usesMattermostConfig
? "test/vitest/vitest.extension-mattermost.config.ts"
: usesMatrixConfig
? "test/vitest/vitest.extension-matrix.config.ts"
: usesMediaConfig
? "test/vitest/vitest.extension-media.config.ts"
: usesMemoryConfig
? "test/vitest/vitest.extension-memory.config.ts"
: usesMessagingConfig
? "test/vitest/vitest.extension-messaging.config.ts"
: usesMiscConfig
? "test/vitest/vitest.extension-misc.config.ts"
: usesMsTeamsConfig
? "test/vitest/vitest.extension-msteams.config.ts"
: usesQaConfig
? "test/vitest/vitest.extension-qa.config.ts"
: usesTelegramConfig
? "test/vitest/vitest.extension-telegram.config.ts"
: usesVoiceCallConfig
? "test/vitest/vitest.extension-voice-call.config.ts"
: usesWhatsAppConfig
? "test/vitest/vitest.extension-whatsapp.config.ts"
: usesZaloConfig
? "test/vitest/vitest.extension-zalo.config.ts"
: usesProviderOpenAiConfig
? "test/vitest/vitest.extension-provider-openai.config.ts"
: usesProviderConfig
? "test/vitest/vitest.extension-providers.config.ts"
: "test/vitest/vitest.extensions.config.ts";
const testFileCount = roots.reduce(
(sum, root) => sum + countTestFiles(path.join(repoRoot, root)),
0,

View File

@@ -121,32 +121,6 @@
}
}
},
{
"name": "@openclaw/bluebubbles",
"description": "OpenClaw BlueBubbles channel plugin",
"source": "official",
"kind": "channel",
"openclaw": {
"channel": {
"id": "bluebubbles",
"label": "BlueBubbles",
"selectionLabel": "BlueBubbles (macOS app)",
"detailLabel": "BlueBubbles",
"docsPath": "/channels/bluebubbles",
"docsLabel": "bluebubbles",
"blurb": "iMessage via the BlueBubbles mac app + REST API.",
"aliases": ["bb"],
"preferOver": ["imessage"],
"systemImage": "bubble.left.and.text.bubble.right",
"order": 75
},
"install": {
"npmSpec": "@openclaw/bluebubbles",
"defaultChoice": "npm",
"minHostVersion": ">=2026.4.10"
}
}
},
{
"name": "@openclaw/discord",
"description": "OpenClaw Discord channel plugin",

View File

@@ -7,7 +7,6 @@ import {
resolveCommandsLightIncludePattern,
} from "../test/vitest/vitest.commands-light-paths.mjs";
import { isAcpxExtensionRoot } from "../test/vitest/vitest.extension-acpx-paths.mjs";
import { isBlueBubblesExtensionRoot } from "../test/vitest/vitest.extension-bluebubbles-paths.mjs";
import { isBrowserExtensionRoot } from "../test/vitest/vitest.extension-browser-paths.mjs";
import { resolveSplitChannelExtensionShard } from "../test/vitest/vitest.extension-channel-split-paths.mjs";
import { isDiffsExtensionRoot } from "../test/vitest/vitest.extension-diffs-paths.mjs";
@@ -72,7 +71,6 @@ const CRON_VITEST_CONFIG = "test/vitest/vitest.cron.config.ts";
const DAEMON_VITEST_CONFIG = "test/vitest/vitest.daemon.config.ts";
const E2E_VITEST_CONFIG = "test/vitest/vitest.e2e.config.ts";
const EXTENSION_ACPX_VITEST_CONFIG = "test/vitest/vitest.extension-acpx.config.ts";
const EXTENSION_BLUEBUBBLES_VITEST_CONFIG = "test/vitest/vitest.extension-bluebubbles.config.ts";
const EXTENSION_BROWSER_VITEST_CONFIG = "test/vitest/vitest.extension-browser.config.ts";
const EXTENSION_CHANNELS_VITEST_CONFIG = "test/vitest/vitest.extension-channels.config.ts";
const EXTENSION_DIFFS_VITEST_CONFIG = "test/vitest/vitest.extension-diffs.config.ts";
@@ -170,7 +168,6 @@ const FULL_SUITE_CONFIG_WEIGHT = new Map([
[UNIT_SECURITY_VITEST_CONFIG, 30],
[UNIT_SUPPORT_VITEST_CONFIG, 28],
[EXTENSION_ZALO_VITEST_CONFIG, 24],
[EXTENSION_BLUEBUBBLES_VITEST_CONFIG, 22],
[EXTENSION_IRC_VITEST_CONFIG, 20],
[EXTENSION_FEISHU_VITEST_CONFIG, 18],
[EXTENSION_MATTERMOST_VITEST_CONFIG, 16],
@@ -252,7 +249,6 @@ const VITEST_CONFIG_BY_KIND = {
extension: EXTENSIONS_VITEST_CONFIG,
extensionFull: FULL_EXTENSIONS_VITEST_CONFIG,
extensionAcpx: EXTENSION_ACPX_VITEST_CONFIG,
extensionBlueBubbles: EXTENSION_BLUEBUBBLES_VITEST_CONFIG,
extensionBrowser: EXTENSION_BROWSER_VITEST_CONFIG,
extensionChannel: EXTENSION_CHANNELS_VITEST_CONFIG,
extensionDiffs: EXTENSION_DIFFS_VITEST_CONFIG,
@@ -1090,9 +1086,6 @@ function classifyTarget(arg, cwd) {
if (isDiffsExtensionRoot(extensionRoot)) {
return "extensionDiffs";
}
if (isBlueBubblesExtensionRoot(extensionRoot)) {
return "extensionBlueBubbles";
}
if (isBrowserExtensionRoot(extensionRoot)) {
return "extensionBrowser";
}
@@ -1398,7 +1391,6 @@ export function buildVitestRunPlans(
"e2e",
"extensionAcpx",
"extensionDiffs",
"extensionBlueBubbles",
"extensionBrowser",
"extensionDiscord",
"extensionFeishu",

View File

@@ -200,8 +200,8 @@ const announceFormatChannelPlugins = [
source: "test",
},
{
pluginId: "bluebubbles",
plugin: createChannelTestPluginBase({ id: "bluebubbles", label: "BlueBubbles" }),
pluginId: "imessage",
plugin: createChannelTestPluginBase({ id: "imessage", label: "iMessage" }),
source: "test",
},
{
@@ -707,10 +707,10 @@ describe("subagent announce formatting", () => {
it("keeps completion delivery enabled for extension channels captured from requester origin", async () => {
const didAnnounce = await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:test",
childRunId: "run-direct-completion-bluebubbles",
childRunId: "run-direct-completion-imessage",
requesterSessionKey: "agent:main:main",
requesterDisplayKey: "main",
requesterOrigin: { channel: "bluebubbles", to: "+1234567890", accountId: "acct-bb" },
requesterOrigin: { channel: "imessage", to: "+1234567890", accountId: "acct-bb" },
...defaultOutcomeAnnounce,
expectsCompletionMessage: true,
});
@@ -720,7 +720,7 @@ describe("subagent announce formatting", () => {
expect(agentSpy).toHaveBeenCalledTimes(1);
const call = agentSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
expect(call?.params?.deliver).toBe(true);
expect(call?.params?.channel).toBe("bluebubbles");
expect(call?.params?.channel).toBe("imessage");
expect(call?.params?.to).toBe("+1234567890");
expect(call?.params?.accountId).toBe("acct-bb");
});
@@ -1591,7 +1591,7 @@ describe("subagent announce formatting", () => {
hasSubagentDeliveryTargetHook = true;
subagentDeliveryTargetHookMock.mockResolvedValueOnce({
origin: {
channel: "bluebubbles",
channel: "imessage",
accountId: "acct-bb",
to: "+1234567890",
},
@@ -1599,7 +1599,7 @@ describe("subagent announce formatting", () => {
const didAnnounce = await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:test",
childRunId: "run-direct-hook-bluebubbles",
childRunId: "run-direct-hook-imessage",
requesterSessionKey: "agent:main:main",
requesterDisplayKey: "main",
requesterOrigin: {
@@ -1617,7 +1617,7 @@ describe("subagent announce formatting", () => {
expect(agentSpy).toHaveBeenCalledTimes(1);
const call = agentSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
expect(call?.params?.deliver).toBe(true);
expect(call?.params?.channel).toBe("bluebubbles");
expect(call?.params?.channel).toBe("imessage");
expect(call?.params?.to).toBe("+1234567890");
expect(call?.params?.accountId).toBe("acct-bb");
});
@@ -2141,9 +2141,9 @@ describe("subagent announce formatting", () => {
const didAnnounce = await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:test",
childRunId: "run-direct-bluebubbles",
childRunId: "run-direct-imessage",
requesterSessionKey: "agent:main:main",
requesterOrigin: { channel: "bluebubbles", accountId: "acct-bb", to: "+1234567890" },
requesterOrigin: { channel: "imessage", accountId: "acct-bb", to: "+1234567890" },
requesterDisplayKey: "main",
...defaultOutcomeAnnounce,
});
@@ -2156,7 +2156,7 @@ describe("subagent announce formatting", () => {
expectFinal?: boolean;
};
expect(call?.params?.deliver).toBe(true);
expect(call?.params?.channel).toBe("bluebubbles");
expect(call?.params?.channel).toBe("imessage");
expect(call?.params?.to).toBe("+1234567890");
expect(call?.params?.accountId).toBe("acct-bb");
expect(call?.expectFinal).toBe(true);
@@ -2949,7 +2949,7 @@ describe("subagent announce formatting", () => {
it("prefers requesterOrigin channel over stale session lastChannel in queued announce", async () => {
embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(false);
embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false);
// Session store has stale whatsapp channel, but the requesterOrigin says bluebubbles.
// Session store has stale whatsapp channel, but the requesterOrigin says imessage.
sessionStore = {
"agent:main:main": {
sessionId: "session-stale",

View File

@@ -1001,11 +1001,11 @@ describe("message tool description", () => {
setActivePluginRegistry(createTestRegistry([]));
});
const bluebubblesPlugin = createChannelPlugin({
id: "bluebubbles",
label: "BlueBubbles",
docsPath: "/channels/bluebubbles",
blurb: "BlueBubbles test plugin.",
const imessagePlugin = createChannelPlugin({
id: "imessage",
label: "iMessage",
docsPath: "/channels/imessage",
blurb: "iMessage test plugin.",
describeMessageTool: ({ currentChannelId }) => {
const all: ChannelMessageActionName[] = [
"react",
@@ -1031,7 +1031,7 @@ describe("message tool description", () => {
},
messaging: {
normalizeTarget: (raw) => {
const trimmed = raw.trim().replace(/^bluebubbles:/i, "");
const trimmed = raw.trim().replace(/^imessage:/i, "");
const lower = trimmed.toLowerCase();
if (lower.startsWith("chat_guid:")) {
const guid = trimmed.slice("chat_guid:".length);
@@ -1059,15 +1059,15 @@ describe("message tool description", () => {
expect(target?.description).toContain("Telegram chat id/@username");
});
it("hides BlueBubbles group actions for DM targets", () => {
it("hides iMessage group actions for DM targets", () => {
setActivePluginRegistry(
createTestRegistry([{ pluginId: "bluebubbles", source: "test", plugin: bluebubblesPlugin }]),
createTestRegistry([{ pluginId: "imessage", source: "test", plugin: imessagePlugin }]),
);
const tool = createMessageTool({
config: {} as never,
currentChannelProvider: "bluebubbles",
currentChannelId: "bluebubbles:chat_guid:iMessage;-;+15551234567",
currentChannelProvider: "imessage",
currentChannelId: "imessage:chat_guid:iMessage;-;+15551234567",
});
expect(tool.description).not.toContain("renameGroup");
@@ -1189,12 +1189,12 @@ describe("message tool description", () => {
it("keeps the current-channel description stable when only one channel is configured", () => {
setActivePluginRegistry(
createTestRegistry([{ pluginId: "bluebubbles", source: "test", plugin: bluebubblesPlugin }]),
createTestRegistry([{ pluginId: "imessage", source: "test", plugin: imessagePlugin }]),
);
const tool = createMessageTool({
config: {} as never,
currentChannelProvider: "bluebubbles",
currentChannelProvider: "imessage",
});
expect(tool.description).toContain("Supports actions:");

View File

@@ -569,7 +569,7 @@ describe("resolveChunkMode", () => {
it.each([
{ cfg: undefined, provider: "telegram", accountId: undefined, expected: "length" },
{ cfg: {}, provider: "discord", accountId: undefined, expected: "length" },
{ cfg: undefined, provider: "bluebubbles", accountId: undefined, expected: "length" },
{ cfg: undefined, provider: "imessage", accountId: undefined, expected: "length" },
{ cfg: providerCfg, provider: "__internal__", accountId: undefined, expected: "length" },
{ cfg: providerCfg, provider: "slack", accountId: undefined, expected: "newline" },
{ cfg: providerCfg, provider: "discord", accountId: undefined, expected: "length" },

View File

@@ -109,8 +109,8 @@ export function installGroupRequireMentionTestPlugins() {
source: "test",
},
{
pluginId: "bluebubbles",
plugin: createChannelTestPluginBase({ id: "bluebubbles" }),
pluginId: "imessage",
plugin: createChannelTestPluginBase({ id: "imessage" }),
source: "test",
},
]),

View File

@@ -1027,7 +1027,7 @@ describe("resolveGroupRequireMention", () => {
it("preserves plugin-backed channel requireMention resolution", async () => {
const cfg: OpenClawConfig = {
channels: {
bluebubbles: {
imessage: {
groups: {
"chat:primary": { requireMention: false },
},
@@ -1035,12 +1035,12 @@ describe("resolveGroupRequireMention", () => {
},
};
const ctx: TemplateContext = {
Provider: "bluebubbles",
From: "bluebubbles:group:chat:primary",
Provider: "imessage",
From: "imessage:group:chat:primary",
};
const groupResolution: GroupKeyResolution = {
key: "bluebubbles:group:chat:primary",
channel: "bluebubbles",
key: "imessage:group:chat:primary",
channel: "imessage",
id: "chat:primary",
chatType: "group",
};

View File

@@ -127,7 +127,7 @@ export function buildThreadingToolContext(params: {
};
}
const provider = normalizeChannelId(rawProvider) ?? normalizeAnyChannelId(rawProvider);
// Fallback for unrecognized/plugin channels (e.g., BlueBubbles before plugin registry init)
// Fallback for unrecognized/plugin channels (e.g., iMessage before plugin registry init)
const threading = provider ? getChannelPlugin(provider)?.threading : undefined;
if (!threading?.buildToolContext) {
return {

View File

@@ -47,7 +47,7 @@ describe("resolveEffectiveBlockStreamingConfig", () => {
it("honors newline chunkMode for plugin channels even before the plugin registry is loaded", () => {
const cfg = {
channels: {
bluebubbles: {
imessage: {
chunkMode: "newline",
},
},
@@ -64,7 +64,7 @@ describe("resolveEffectiveBlockStreamingConfig", () => {
const resolved = resolveEffectiveBlockStreamingConfig({
cfg,
provider: "bluebubbles",
provider: "imessage",
});
expect(resolved.chunking.flushOnParagraph).toBe(true);

View File

@@ -201,7 +201,7 @@ function resolveFirstConversationTargetForTest(params: {
function parsePrefixedConversationIdForTest(
raw: string | undefined | null,
channel: "bluebubbles" | "imessage",
channel: "imessage",
): string | undefined {
const trimmed = raw
?.trim()
@@ -212,7 +212,7 @@ function parsePrefixedConversationIdForTest(
function resolvePrefixedConversationIdForTest(
targets: Array<string | undefined | null>,
channel: "bluebubbles" | "imessage",
channel: "imessage",
): string | undefined {
return targets.map((target) => parsePrefixedConversationIdForTest(target, channel)).find(Boolean);
}
@@ -318,30 +318,6 @@ function setMinimalAcpCommandRegistryForTests(): void {
},
},
},
{
pluginId: "bluebubbles",
source: "test",
plugin: {
...createChannelTestPluginBase({ id: "bluebubbles", label: "BlueBubbles" }),
bindings: {
resolveCommandConversation: ({
originatingTo,
commandTo,
fallbackTo,
}: {
originatingTo?: string;
commandTo?: string;
fallbackTo?: string;
}) => {
const conversationId = resolvePrefixedConversationIdForTest(
[originatingTo, commandTo, fallbackTo],
"bluebubbles",
);
return conversationId ? { conversationId } : null;
},
},
},
},
{
pluginId: "imessage",
source: "test",
@@ -423,7 +399,7 @@ function setMinimalAcpCommandRegistryForTests(): void {
},
},
},
...(["bluebubbles", "imessage", "feishu", "line"] as const).map((channelId) => ({
...(["feishu", "line"] as const).map((channelId) => ({
pluginId: channelId,
source: "test",
plugin: {
@@ -791,20 +767,6 @@ async function runLineDmAcpCommand(commandBody: string, cfg: OpenClawConfig = ba
);
}
async function runBlueBubblesDmAcpCommand(commandBody: string, cfg: OpenClawConfig = baseCfg) {
return handleAcpCommand(
createConversationParams(
commandBody,
{
channel: "bluebubbles",
originatingTo: "bluebubbles:+15555550123",
},
cfg,
),
true,
);
}
async function runIMessageDmAcpCommand(commandBody: string, cfg: OpenClawConfig = baseCfg) {
return handleAcpCommand(
createConversationParams(
@@ -1199,15 +1161,15 @@ describe("/acp command", () => {
);
});
it("binds BlueBubbles DMs with --bind here", async () => {
const result = await runBlueBubblesDmAcpCommand("/acp spawn codex --bind here");
it("binds iMessage DMs with --bind here", async () => {
const result = await runIMessageDmAcpCommand("/acp spawn codex --bind here");
expect(result?.reply?.text).toContain("Bound this conversation to");
expect(hoisted.sessionBindingBindMock).toHaveBeenCalledWith(
expect.objectContaining({
placement: "current",
conversation: expect.objectContaining({
channel: "bluebubbles",
channel: "imessage",
accountId: "default",
conversationId: "+15555550123",
}),

View File

@@ -75,15 +75,6 @@ function parseFeishuDirectConversationIdForTest(raw?: string | null): string | u
return trimmed.replace(/^(user|dm):/i, "").trim() || undefined;
}
function parseBlueBubblesConversationIdFromTargetForTest(raw?: string | null): string | undefined {
const trimmed = raw?.trim().replace(/^bluebubbles:/i, "");
if (!trimmed) {
return undefined;
}
const prefixed = /^(chat_guid|chat_identifier|chat_id):(.+)$/i.exec(trimmed);
return (prefixed?.[2] ?? trimmed).trim() || undefined;
}
function parseIMessageConversationIdFromTargetForTest(raw?: string | null): string | undefined {
const trimmed = raw?.trim().replace(/^imessage:/i, "");
if (!trimmed) {
@@ -294,30 +285,6 @@ function setMinimalAcpContextRegistryForTests(): void {
},
},
},
{
pluginId: "bluebubbles",
source: "test",
plugin: {
...createChannelTestPluginBase({ id: "bluebubbles", label: "BlueBubbles" }),
bindings: {
resolveCommandConversation: ({
originatingTo,
commandTo,
fallbackTo,
}: {
originatingTo?: string;
commandTo?: string;
fallbackTo?: string;
}) => {
const conversationId =
parseBlueBubblesConversationIdFromTargetForTest(originatingTo) ??
parseBlueBubblesConversationIdFromTargetForTest(commandTo) ??
parseBlueBubblesConversationIdFromTargetForTest(fallbackTo);
return conversationId ? { conversationId } : null;
},
},
},
},
{
pluginId: "imessage",
source: "test",
@@ -698,16 +665,16 @@ describe("commands-acp context", () => {
expect(resolveAcpCommandParentConversationId(params)).toBe("!room:example.org");
});
it("resolves BlueBubbles DM conversation ids from current targets", () => {
it("resolves iMessage DM conversation ids from current targets", () => {
const params = buildCommandTestParams("/acp status", baseCfg, {
Provider: "bluebubbles",
Surface: "bluebubbles",
OriginatingChannel: "bluebubbles",
OriginatingTo: "bluebubbles:+15555550123",
Provider: "imessage",
Surface: "imessage",
OriginatingChannel: "imessage",
OriginatingTo: "imessage:+15555550123",
});
expect(resolveAcpCommandBindingContext(params)).toEqual({
channel: "bluebubbles",
channel: "imessage",
accountId: "default",
threadId: undefined,
conversationId: "+15555550123",
@@ -716,17 +683,17 @@ describe("commands-acp context", () => {
expect(resolveAcpCommandConversationId(params)).toBe("+15555550123");
});
it("resolves BlueBubbles group conversation ids from explicit chat targets", () => {
it("resolves iMessage group conversation ids from explicit chat targets", () => {
const params = buildCommandTestParams("/acp status", baseCfg, {
Provider: "bluebubbles",
Surface: "bluebubbles",
OriginatingChannel: "bluebubbles",
OriginatingTo: "bluebubbles:chat_guid:iMessage;+;chat123",
Provider: "imessage",
Surface: "imessage",
OriginatingChannel: "imessage",
OriginatingTo: "imessage:chat_guid:iMessage;+;chat123",
AccountId: "work",
});
expect(resolveAcpCommandBindingContext(params)).toEqual({
channel: "bluebubbles",
channel: "imessage",
accountId: "work",
threadId: undefined,
conversationId: "iMessage;+;chat123",

View File

@@ -23,7 +23,6 @@ const BUNDLED_EXTENSION_IDS = [...bundledPluginRoots.keys()].toSorted(
(left, right) => right.length - left.length,
);
const GUARDED_CHANNEL_EXTENSIONS = new Set([
"bluebubbles",
"discord",
"feishu",
"googlechat",
@@ -196,7 +195,6 @@ const CHANNEL_CONFIG_SCHEMA_GUARDS: GuardedSource[] = [
const LOCAL_EXTENSION_API_BARREL_GUARDS = [
"acpx",
"bluebubbles",
"device-pair",
"diagnostics-otel",
"diagnostics-prometheus",

View File

@@ -10,7 +10,6 @@ export const channelPluginSurfaceKeys = [
] as const;
export const sessionBindingContractChannelIds = [
"bluebubbles",
"discord",
"feishu",
"imessage",

View File

@@ -125,13 +125,6 @@ type ChannelConversationBindingManagerFactory = NonNullable<
NonNullable<ChannelPlugin["conversationBindings"]>["createManager"]
>;
type BlueBubblesContractApi = {
blueBubblesConversationBindingTesting: {
resetBlueBubblesConversationBindingsForTests: () => void;
};
createBlueBubblesConversationBindingManager: ChannelConversationBindingManagerFactory;
};
type DiscordContractApi = {
createThreadBindingManager: (params: {
accountId: string;
@@ -216,15 +209,6 @@ function setRegistryBackedConversationBindingPlugin(params: {
);
}
async function prepareBlueBubblesSessionBindingContract() {
const api = await getContractApi<BlueBubblesContractApi>("bluebubbles");
api.blueBubblesConversationBindingTesting.resetBlueBubblesConversationBindingsForTests();
setRegistryBackedConversationBindingPlugin({
id: "bluebubbles",
createManager: api.createBlueBubblesConversationBindingManager,
});
}
async function prepareDiscordSessionBindingContract() {
const api = await getContractApi<DiscordContractApi>("discord");
api.discordThreadBindingTesting.resetThreadBindingsForTests();
@@ -258,69 +242,6 @@ const sessionBindingContractEntries: Record<
SessionBindingContractChannelId,
Omit<SessionBindingContractEntry, "id">
> = {
bluebubbles: {
beforeEach: prepareBlueBubblesSessionBindingContract,
expectedCapabilities: {
adapterAvailable: true,
bindSupported: true,
unbindSupported: true,
placements: ["current"],
},
getCapabilities: () => {
void createChannelConversationBindingManager({
channelId: "bluebubbles",
cfg: baseSessionBindingCfg,
accountId: "default",
});
return getSessionBindingService().getCapabilities({
channel: "bluebubbles",
accountId: "default",
});
},
bindAndResolve: async () => {
await createChannelConversationBindingManager({
channelId: "bluebubbles",
cfg: baseSessionBindingCfg,
accountId: "default",
});
const service = getSessionBindingService();
const binding = await service.bind({
targetSessionKey: "agent:codex:acp:binding:bluebubbles:default:abc123",
targetKind: "session",
conversation: {
channel: "bluebubbles",
accountId: "default",
conversationId: "+15555550123",
},
placement: "current",
metadata: {
agentId: "codex",
label: "codex-main",
},
});
expectResolvedSessionBinding({
channel: "bluebubbles",
accountId: "default",
conversationId: "+15555550123",
targetSessionKey: "agent:codex:acp:binding:bluebubbles:default:abc123",
});
return binding;
},
unbindAndVerify: unbindAndExpectClearedSessionBinding,
cleanup: async () => {
const manager = await createChannelConversationBindingManager({
channelId: "bluebubbles",
cfg: baseSessionBindingCfg,
accountId: "default",
});
await manager?.stop();
expectClearedSessionBinding({
channel: "bluebubbles",
accountId: "default",
conversationId: "+15555550123",
});
},
},
discord: {
beforeEach: prepareDiscordSessionBindingContract,
expectedCapabilities: {

View File

@@ -11,7 +11,6 @@ type DirectoryContractRef = {
};
const threadingContractPluginIds = new Set<ChannelId>([
"bluebubbles",
"discord",
"googlechat",
"matrix",

View File

@@ -705,7 +705,7 @@ describe("promptParsedAllowFromForAccount", () => {
const next = await promptParsedAllowFromForAccount({
cfg: {
channels: {
bluebubbles: {
imessage: {
accounts: {
alt: {
allowFrom: ["old"],
@@ -717,7 +717,7 @@ describe("promptParsedAllowFromForAccount", () => {
accountId: "alt",
defaultAccountId: DEFAULT_ACCOUNT_ID,
prompter,
noteTitle: "BlueBubbles allowlist",
noteTitle: "iMessage allowlist",
noteLines: ["line"],
message: "msg",
placeholder: "placeholder",
@@ -725,7 +725,7 @@ describe("promptParsedAllowFromForAccount", () => {
parseSetupEntriesWithParser(raw, (entry) => ({ value: entry.toLowerCase() })),
getExistingAllowFrom: ({ cfg, accountId }) => [
...((
cfg.channels?.bluebubbles?.accounts?.[accountId] as
cfg.channels?.imessage?.accounts?.[accountId] as
| { allowFrom?: ReadonlyArray<string | number> }
| undefined
)?.allowFrom ?? []),
@@ -733,7 +733,7 @@ describe("promptParsedAllowFromForAccount", () => {
applyAllowFrom: ({ cfg, accountId, allowFrom }) =>
patchChannelConfigForAccount({
cfg,
channel: "bluebubbles",
channel: "imessage",
accountId,
patch: { allowFrom },
}),
@@ -741,12 +741,12 @@ describe("promptParsedAllowFromForAccount", () => {
expect(
(
next.channels?.bluebubbles?.accounts?.alt as
next.channels?.imessage?.accounts?.alt as
| { allowFrom?: ReadonlyArray<string | number> }
| undefined
)?.allowFrom,
).toEqual(["alice"]);
expect(prompter.note).toHaveBeenCalledWith("line", "BlueBubbles allowlist");
expect(prompter.note).toHaveBeenCalledWith("line", "iMessage allowlist");
});
it("can merge parsed values with existing entries", async () => {
@@ -788,7 +788,7 @@ describe("createPromptParsedAllowFromForAccount", () => {
parseEntries: (raw) => ({ entries: [raw.trim().toLowerCase()] }),
getExistingAllowFrom: ({ cfg, accountId }) => [
...((
cfg.channels?.bluebubbles?.accounts?.[accountId] as
cfg.channels?.imessage?.accounts?.[accountId] as
| { allowFrom?: ReadonlyArray<string | number> }
| undefined
)?.allowFrom ?? []),
@@ -796,7 +796,7 @@ describe("createPromptParsedAllowFromForAccount", () => {
applyAllowFrom: ({ cfg, accountId, allowFrom }) =>
patchChannelConfigForAccount({
cfg,
channel: "bluebubbles",
channel: "imessage",
accountId,
patch: { allowFrom },
}),
@@ -806,7 +806,7 @@ describe("createPromptParsedAllowFromForAccount", () => {
const next = await promptAllowFrom({
cfg: {
channels: {
bluebubbles: {
imessage: {
accounts: {
work: {
allowFrom: ["old"],
@@ -820,7 +820,7 @@ describe("createPromptParsedAllowFromForAccount", () => {
expect(
(
next.channels?.bluebubbles?.accounts?.work as
next.channels?.imessage?.accounts?.work as
| { allowFrom?: ReadonlyArray<string | number> }
| undefined
)?.allowFrom,

View File

@@ -31,8 +31,8 @@ describe("resolveChannelTtsVoiceDelivery", () => {
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "bluebubbles",
plugin: createChannelPlugin("bluebubbles", {
pluginId: "imessage",
plugin: createChannelPlugin("imessage", {
chatTypes: ["direct"],
tts: {
voice: {
@@ -85,7 +85,7 @@ describe("resolveChannelTtsVoiceDelivery", () => {
},
]),
);
expect(resolveChannelTtsVoiceDelivery("bluebubbles")).toEqual({
expect(resolveChannelTtsVoiceDelivery("imessage")).toEqual({
synthesisTarget: "audio-file",
audioFileFormats: ["mp3", "caf", "audio/mpeg", "audio/x-caf"],
});

View File

@@ -277,7 +277,7 @@ export type ChannelGroupContext = {
* Container tokens (file-extension shape, no leading dot) that the host
* speech-core pipeline knows how to pre-transcode synthesized audio into.
* Channels that benefit from a specific container — currently only
* BlueBubbles, which needs Apple's native voice-memo CAF descriptor — name
* iMessage, which needs Apple's native voice-memo CAF descriptor — name
* one here. Adding a new entry requires extending the host transcoder
* recipe table in lockstep so a typed declaration cannot silently no-op.
*/
@@ -292,7 +292,7 @@ export type ChannelTtsVoiceDeliveryCapabilities = {
* delivery. When set and the host can transcode (e.g. `afconvert` on
* macOS), the TTS pipeline pre-encodes synthesized audio to this format
* before handing it to the channel. Useful for channels (such as
* BlueBubbles) whose downstream attempts its own container conversion
* iMessage) whose downstream attempts its own container conversion
* that races against the upload write and fails.
*/
preferAudioFileFormat?: PreferredAudioFileFormat;

File diff suppressed because one or more lines are too long

View File

@@ -97,7 +97,7 @@ describe("config plugin validation", () => {
let suiteHome = "";
let badPluginDir = "";
let enumPluginDir = "";
let bluebubblesPluginDir = "";
let chatPluginDir = "";
let googleOverridePluginDir = "";
let voiceCallSchemaPluginDir = "";
let bundlePluginDir = "";
@@ -135,7 +135,7 @@ describe("config plugin validation", () => {
await mkdirSafe(suiteHome);
badPluginDir = path.join(suiteHome, "bad-plugin");
enumPluginDir = path.join(suiteHome, "enum-plugin");
bluebubblesPluginDir = path.join(suiteHome, "bluebubbles-plugin");
chatPluginDir = path.join(suiteHome, "chat-plugin");
await writePluginFixture({
dir: badPluginDir,
id: "bad-plugin",
@@ -163,9 +163,9 @@ describe("config plugin validation", () => {
},
});
await writePluginFixture({
dir: bluebubblesPluginDir,
id: "bluebubbles-plugin",
channels: ["bluebubbles"],
dir: chatPluginDir,
id: "chat-plugin",
channels: ["chat"],
schema: { type: "object" },
});
googleOverridePluginDir = path.join(suiteHome, "google");
@@ -640,7 +640,7 @@ describe("config plugin validation", () => {
it("does not auto-allow config-loaded overrides of bundled web search plugin ids", async () => {
const res = validateInSuite({
plugins: {
allow: ["bluebubbles", "memory-core"],
allow: ["imessage", "memory-core"],
load: {
paths: [googleOverridePluginDir],
},
@@ -909,8 +909,8 @@ describe("config plugin validation", () => {
it("accepts plugin heartbeat targets", async () => {
const res = validateInSuite({
agents: { defaults: { heartbeat: { target: "bluebubbles" } }, list: [{ id: "pi" }] },
plugins: { enabled: false, load: { paths: [bluebubblesPluginDir] } },
agents: { defaults: { heartbeat: { target: "chat" } }, list: [{ id: "pi" }] },
plugins: { enabled: false, load: { paths: [chatPluginDir] } },
});
expect(res.ok).toBe(true);
});

View File

@@ -244,7 +244,7 @@ describe("web search provider config", () => {
it("does not warn for brave plugin config when bundled web search allowlist compat applies", () => {
const res = validateConfigObjectWithPlugins({
plugins: {
allow: ["bluebubbles", "memory-core"],
allow: ["imessage", "memory-core"],
entries: {
brave: {
config: {

View File

@@ -445,13 +445,12 @@ describe("config io write prepare", () => {
).toBeUndefined();
});
it("keeps plugin AJV defaults out of the persisted candidate", () => {
it("keeps runtime-only channel defaults out of the persisted candidate", () => {
const sourceConfig = {
gateway: { port: 18789 },
channels: {
bluebubbles: {
serverUrl: "http://localhost:1234",
password: "test-password",
imessage: {
cliPath: "/usr/local/bin/imsg",
},
},
} satisfies OpenClawConfig;
@@ -459,10 +458,9 @@ describe("config io write prepare", () => {
const runtimeConfig: OpenClawConfig = {
gateway: { port: 18789 },
channels: {
bluebubbles: {
serverUrl: "http://localhost:1234",
password: "test-password",
enrichGroupParticipantsFromContacts: true,
imessage: {
cliPath: "/usr/local/bin/imsg",
runtimeOnlyDefault: true,
},
},
} satisfies OpenClawConfig;
@@ -484,10 +482,9 @@ describe("config io write prepare", () => {
auth: { mode: "token" },
});
const channels = persisted.channels as Record<string, Record<string, unknown>> | undefined;
expect(channels?.bluebubbles).toBeDefined();
expect(channels?.bluebubbles).not.toHaveProperty("enrichGroupParticipantsFromContacts");
expect(channels?.bluebubbles?.serverUrl).toBe("http://localhost:1234");
expect(channels?.bluebubbles?.password).toBe("test-password");
expect(channels?.imessage).toBeDefined();
expect(channels?.imessage).not.toHaveProperty("runtimeOnlyDefault");
expect(channels?.imessage?.cliPath).toBe("/usr/local/bin/imsg");
});
it("does not reintroduce legacy nested dm.policy defaults in the persisted candidate", () => {

View File

@@ -7,7 +7,6 @@ import {
} from "./plugin-auto-enable.js";
import {
makeApnChannelConfig,
makeBluebubblesAndImessageChannels,
makeIsolatedEnv,
makeRegistry,
makeTempDir,
@@ -27,18 +26,6 @@ function applyWithApnChannelConfig(extra?: {
});
}
function applyWithBluebubblesImessageConfig(extra?: {
plugins?: { entries?: Record<string, { enabled: boolean }>; deny?: string[] };
}) {
return applyPluginAutoEnable({
config: {
channels: makeBluebubblesAndImessageChannels(),
...(extra?.plugins ? { plugins: extra.plugins } : {}),
},
env: makeIsolatedEnv(),
});
}
beforeEach(() => {
resetPluginAutoEnableTestState();
});
@@ -417,45 +404,6 @@ describe("applyPluginAutoEnable channels", () => {
);
});
it("prefers bluebubbles: skips imessage auto-configure when both are configured", () => {
const result = applyWithBluebubblesImessageConfig();
expect(result.config.channels?.bluebubbles?.enabled).toBe(true);
expect(result.config.plugins?.entries?.imessage?.enabled).toBe(false);
expect(result.changes.join("\n")).toContain("BlueBubbles configured, enabled automatically.");
expect(result.changes.join("\n")).not.toContain(
"iMessage configured, enabled automatically.",
);
});
it("keeps imessage enabled if already explicitly enabled (non-destructive)", () => {
const result = applyWithBluebubblesImessageConfig({
plugins: { entries: { imessage: { enabled: true } } },
});
expect(result.config.channels?.bluebubbles?.enabled).toBe(true);
expect(result.config.plugins?.entries?.imessage?.enabled).toBe(true);
});
it("allows imessage auto-configure when bluebubbles is explicitly disabled", () => {
const result = applyWithBluebubblesImessageConfig({
plugins: { entries: { bluebubbles: { enabled: false } } },
});
expect(result.config.plugins?.entries?.bluebubbles?.enabled).toBe(false);
expect(result.config.channels?.imessage?.enabled).toBe(true);
expect(result.changes.join("\n")).toContain("iMessage configured, enabled automatically.");
});
it("allows imessage auto-configure when bluebubbles is in deny list", () => {
const result = applyWithBluebubblesImessageConfig({
plugins: { deny: ["bluebubbles"] },
});
expect(result.config.plugins?.entries?.bluebubbles).toBeUndefined();
expect(result.config.channels?.imessage?.enabled).toBe(true);
});
it("auto-enables imessage when only imessage is configured", () => {
const result = applyPluginAutoEnable({
config: {

View File

@@ -72,10 +72,3 @@ export function makeRegistry(
export function makeApnChannelConfig() {
return { channels: { apn: { someKey: "value" } } };
}
export function makeBluebubblesAndImessageChannels() {
return {
bluebubbles: { serverUrl: "http://localhost:1234", password: "x" },
imessage: { cliPath: "/usr/local/bin/imsg" },
};
}

View File

@@ -10,7 +10,6 @@ import { sensitive } from "./zod-schema.sensitive.js";
const { collectMatchingSchemaPaths, mapSensitivePaths } = __test__;
const BUNDLED_CHANNEL_HINT_PREFIXES = [
"channels.bluebubbles",
"channels.discord",
"channels.imessage",
"channels.irc",

View File

@@ -68,8 +68,8 @@ describe("config schema", () => {
heartbeatChannelInput = {
channels: [
{
id: "bluebubbles",
label: "BlueBubbles",
id: "imessage",
label: "iMessage",
configSchema: { type: "object" },
},
],
@@ -276,9 +276,9 @@ describe("config schema", () => {
const defaultsHint = res.uiHints["agents.defaults.heartbeat.target"];
const listHint = res.uiHints["agents.list.*.heartbeat.target"];
expect(defaultsHint?.help).toContain("bluebubbles");
expect(defaultsHint?.help).toContain("imessage");
expect(defaultsHint?.help).toContain("last");
expect(listHint?.help).toContain("bluebubbles");
expect(listHint?.help).toContain("imessage");
});
it("caches merged schemas for identical plugin/channel metadata", () => {

View File

@@ -216,18 +216,18 @@ describe("appendAssistantMessageToSessionTranscript", () => {
});
it("finds session entry using normalized (lowercased) key", async () => {
const storeKey = "agent:main:bluebubbles:direct:+15551234567";
const storeKey = "agent:main:imessage:direct:+15551234567";
const store = {
[storeKey]: {
sessionId: "test-session-normalized",
chatType: "direct",
channel: "bluebubbles",
channel: "imessage",
},
};
fs.writeFileSync(fixture.storePath(), JSON.stringify(store), "utf-8");
const result = await appendAssistantMessageToSessionTranscript({
sessionKey: "agent:main:BlueBubbles:direct:+15551234567",
sessionKey: "agent:main:iMessage:direct:+15551234567",
text: "Hello normalized!",
storePath: fixture.storePath(),
});

View File

@@ -182,7 +182,7 @@ describe("validateConfigObjectRawWithPlugins channel metadata", () => {
it("still injects channel AJV defaults even in raw mode — persistence safety is handled by io.ts", async () => {
// Channel and plugin AJV validation always runs with applyDefaults: true
// (hardcoded) to avoid breaking schemas that mark defaulted fields as
// required (e.g., BlueBubbles enrichGroupParticipantsFromContacts).
// required.
//
// The actual protection against leaking these defaults to disk lives in
// writeConfigFile (io.ts), which uses persistCandidate (the pre-validation

View File

@@ -173,8 +173,8 @@ describe("resolveCronDeliveryPlan", () => {
it("does not treat channel-owned service prefixes as provider selection", () => {
setCronDeliveryTestRegistry([
{
pluginId: "bluebubbles",
plugin: createPrefixOnlyChannelPlugin("bluebubbles", ["bluebubbles"]),
pluginId: "imessage",
plugin: createPrefixOnlyChannelPlugin("imessage", ["imessage"]),
},
{ pluginId: "imessage", plugin: createPrefixOnlyChannelPlugin("imessage") },
]);

View File

@@ -85,7 +85,7 @@ describe("resolveChannelSetupSelectionContributions", () => {
vi.clearAllMocks();
listChatChannels.mockReturnValue([
makeMeta("discord", "Discord"),
makeMeta("bluebubbles", "BlueBubbles"),
makeMeta("imessage", "iMessage"),
]);
resolveChannelSetupEntries.mockReturnValue(makeChannelSetupEntries());
formatChannelPrimerLine.mockImplementation(
@@ -121,11 +121,11 @@ describe("resolveChannelSetupSelectionContributions", () => {
},
},
{
id: "bluebubbles",
id: "imessage",
meta: {
id: "bluebubbles",
label: "BlueBubbles",
selectionLabel: "BlueBubbles (macOS app)",
id: "imessage",
label: "iMessage",
selectionLabel: "iMessage (macOS app)",
},
},
],
@@ -134,7 +134,7 @@ describe("resolveChannelSetupSelectionContributions", () => {
});
expect(contributions.map((contribution) => contribution.option.label)).toEqual([
"BlueBubbles (macOS app)",
"iMessage (macOS app)",
"Discord (Bot API)",
"Zalo (Bot API)",
]);

View File

@@ -672,7 +672,7 @@ describe("setupChannels workspace shadow exclusion", () => {
"declares an already-installed plugin whose runtime cannot be loaded",
async () => {
// Regression: users who uninstalled an externalized channel plugin
// (qqbot / bluebubbles / discord / ...) while a non-empty
// (qqbot / imessage / discord / ...) while a non-empty
// `channels.<id>` entry remained in their config got dead-ended with
// "<channel> plugin not available" because the installed-catalog
// branch did not fall back to the catalog install flow.

View File

@@ -640,7 +640,7 @@ export async function setupChannels(
// disk keeps it out of `installedCatalogEntries`. Before falling back
// to the bundled-plugin enable path, consult the catalog directly so
// users with a stale config entry for an externalized channel (qqbot,
// bluebubbles, discord, whatsapp, ...) still get auto-install instead
// imessage, discord, whatsapp, ...) still get auto-install instead
// of a dead-end "plugin not available" note.
const fallbackCatalogEntry = getTrustedChannelPluginCatalogEntry(channel, {
cfg: next,

View File

@@ -48,7 +48,7 @@ describe("classifyControlUiRequest", () => {
},
{
name: "falls through non-read requests",
pathname: "/bluebubbles-webhook",
pathname: "/imessage-webhook",
method: "POST",
expected: { kind: "not-control-ui" as const },
},

View File

@@ -1100,7 +1100,7 @@ describe("handleControlUiHttpRequest", () => {
it("does not handle POST to root-mounted paths (plugin webhook passthrough)", async () => {
await withControlUiRoot({
fn: async (tmp) => {
for (const webhookPath of ["/bluebubbles-webhook", "/custom-webhook", "/callback"]) {
for (const webhookPath of ["/imessage-webhook", "/custom-webhook", "/callback"]) {
const { res } = makeMockHttpResponse();
const handled = await handleControlUiHttpRequest(
{ url: webhookPath, method: "POST" } as IncomingMessage,
@@ -1120,7 +1120,7 @@ describe("handleControlUiHttpRequest", () => {
fn: async (tmp) => {
const { res } = makeMockHttpResponse();
const handled = await handleControlUiHttpRequest(
{ url: "/bluebubbles-webhook", method: "POST" } as IncomingMessage,
{ url: "/imessage-webhook", method: "POST" } as IncomingMessage,
res,
{ basePath: "/openclaw", root: { kind: "resolved", path: tmp } },
);
@@ -1163,7 +1163,7 @@ describe("handleControlUiHttpRequest", () => {
await withControlUiRoot({
fn: async (tmp) => {
const { handled, end } = await runControlUiRequest({
url: "/webhook/bluebubbles",
url: "/webhook/imessage",
method: "POST",
rootPath: tmp,
});

View File

@@ -623,7 +623,7 @@ describe("gateway plugin HTTP auth boundary", () => {
test("passes POST webhook routes through root-mounted control ui to plugins", async () => {
const handlePluginRequest = vi.fn(async (req: IncomingMessage, res: ServerResponse) => {
const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
if (req.method !== "POST" || pathname !== "/bluebubbles-webhook") {
if (req.method !== "POST" || pathname !== "/imessage-webhook") {
return false;
}
res.statusCode = 200;
@@ -637,7 +637,7 @@ describe("gateway plugin HTTP auth boundary", () => {
handlePluginRequest,
run: async (server) => {
const response = await sendRequest(server, {
path: "/bluebubbles-webhook",
path: "/imessage-webhook",
method: "POST",
});

View File

@@ -332,7 +332,7 @@ describe("createGatewayPluginRequestHandler", () => {
setActivePluginRegistry(laterActiveRegistry);
const unregister = registerPluginHttpRoute({
path: "/bluebubbles-webhook",
path: "/imessage-webhook",
auth: "plugin",
handler: routeHandler,
});
@@ -344,7 +344,7 @@ describe("createGatewayPluginRequestHandler", () => {
});
const { res } = makeMockHttpResponse();
const handled = await handler({ url: "/bluebubbles-webhook" } as IncomingMessage, res);
const handled = await handler({ url: "/imessage-webhook" } as IncomingMessage, res);
expect(handled).toBe(true);
expect(routeHandler).toHaveBeenCalledTimes(1);
expect(laterActiveRegistry.httpRoutes).toHaveLength(0);
@@ -367,7 +367,7 @@ describe("createGatewayPluginRequestHandler", () => {
pinActivePluginHttpRouteRegistry(startupRegistry);
const unregister = registerPluginHttpRoute({
path: "/bluebubbles-webhook",
path: "/imessage-webhook",
auth: "plugin",
handler: routeHandler,
});
@@ -379,7 +379,7 @@ describe("createGatewayPluginRequestHandler", () => {
});
const { res } = makeMockHttpResponse();
const handled = await handler({ url: "/bluebubbles-webhook" } as IncomingMessage, res);
const handled = await handler({ url: "/imessage-webhook" } as IncomingMessage, res);
expect(handled).toBe(true);
expect(routeHandler).toHaveBeenCalledTimes(1);
expect(staleExplicitRegistry.httpRoutes).toHaveLength(1);

View File

@@ -115,9 +115,9 @@ export function createDefaultGatewayTestChannels() {
plugin: createStubChannelPlugin({ id: "zalouser", label: "Zalo Personal" }),
},
{
pluginId: "bluebubbles",
pluginId: "imessage",
source: "test" as const,
plugin: createStubChannelPlugin({ id: "bluebubbles", label: "BlueBubbles" }),
plugin: createStubChannelPlugin({ id: "imessage", label: "iMessage" }),
},
];
}

View File

@@ -122,7 +122,7 @@ describe("mime detection", () => {
// CAF files start with the four-byte ASCII tag "caff". `file-type` v22 has
// no native CAF detector, so without the manual magic-byte fallback the
// host-local-media validator drops `afconvert`-produced voice-memo CAFs as
// unknown binary blobs. Regression guard for the BlueBubbles voice-memo
// unknown binary blobs. Regression guard for the iMessage voice-memo
// pre-transcode path.
const buf = Buffer.concat([Buffer.from("caff", "ascii"), Buffer.alloc(60)]);
const mime = await detectMime({ buffer: buf });

View File

@@ -65,7 +65,7 @@ describe("createChannelReplyPipeline", () => {
const pipeline = createChannelReplyPipeline({
cfg: {},
agentId: "main",
channel: "bluebubbles",
channel: "imessage",
typingCallbacks: {
onReplyStart,
onIdle,

View File

@@ -108,9 +108,3 @@ export type ReplyPrefixOptions = ReplyPrefixOptionsCompat;
export type SourceReplyDeliveryMode = SourceReplyDeliveryModeCompat;
/** @deprecated Use `openclaw/plugin-sdk/channel-message`. */
export type TypingCallbacks = TypingCallbacksCompat;
export {
resolveBlueBubblesGroupRequireMention,
resolveBlueBubblesGroupToolPolicy,
} from "./bluebubbles-policy.js";
export { collectBlueBubblesStatusIssues } from "./bluebubbles.js";

View File

@@ -114,7 +114,7 @@ describe("config footprint guardrails", () => {
});
it("keeps bundled channel private-network config canonical in generated metadata", () => {
const pluginIds = ["bluebubbles", "matrix", "nextcloud-talk", "tlon"];
const pluginIds = ["matrix", "nextcloud-talk", "tlon"];
for (const pluginId of pluginIds) {
const metadata = GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA.find(

View File

@@ -3,7 +3,6 @@ import { describePackageManifestContract } from "openclaw/plugin-sdk/plugin-test
type PackageManifestContractParams = Parameters<typeof describePackageManifestContract>[0];
const packageManifestContractTests: PackageManifestContractParams[] = [
{ pluginId: "bluebubbles", minHostVersionBaseline: "2026.3.22" },
{
pluginId: "discord",
pluginLocalRuntimeDeps: ["@discordjs/voice", "discord-api-types", "opusscript"],

View File

@@ -23,7 +23,7 @@ const PRIVATE_BUNDLED_SDK_SURFACE_PATTERN =
/\b(?:Private helper surface|Narrow plugin-sdk surface for the bundled|Narrow .*runtime exports used by the bundled)\b/i;
const GENERIC_CORE_HELPER_FILES = ["src/polls.ts", "src/poll-params.ts"] as const;
const GENERIC_CORE_PLUGIN_OWNER_NAME_PATTERN =
/\b(?:bluebubbles|discord|feishu|googlechat|matrix|mattermost|msteams|slack|telegram|whatsapp|zalo|zalouser)\b/gi;
/\b(?:imessage|discord|feishu|googlechat|matrix|mattermost|msteams|slack|telegram|whatsapp|zalo|zalouser)\b/gi;
const PACKAGE_CONTRACT_SCAN_TIMEOUT_MS = 240_000;
const DEPRECATED_EXTENSION_SDK_SPECIFIERS = new Set([
"openclaw/plugin-sdk",

View File

@@ -214,13 +214,13 @@ describe("registerPluginHttpRoute", () => {
setActivePluginRegistry(laterActiveRegistry);
const unregister = registerPluginHttpRoute({
path: "/bluebubbles-webhook",
path: "/imessage-webhook",
auth: "plugin",
handler: vi.fn(),
});
expectRegisteredRouteShape(startupRegistry, {
path: "/bluebubbles-webhook",
path: "/imessage-webhook",
auth: "plugin",
});
expect(laterActiveRegistry.httpRoutes).toHaveLength(0);

View File

@@ -133,7 +133,7 @@ describe("plugin runtime route registry", () => {
},
{
name: "prefers the pinned route registry when it already owns routes",
pinnedRegistry: createRegistryWithRoute("/bluebubbles-webhook"),
pinnedRegistry: createRegistryWithRoute("/imessage-webhook"),
explicitRegistry: createRegistryWithRoute("/plugins/diffs"),
expected: "pinned",
},

View File

@@ -23,8 +23,8 @@ describe("syncPluginVersions", () => {
name: "openclaw",
version: "2026.4.1",
});
writeJson(path.join(rootDir, "extensions/bluebubbles/package.json"), {
name: "@openclaw/bluebubbles",
writeJson(path.join(rootDir, "extensions/imessage/package.json"), {
name: "@openclaw/imessage",
version: "2026.3.30",
devDependencies: {
openclaw: "workspace:*",
@@ -47,7 +47,7 @@ describe("syncPluginVersions", () => {
const summary = syncPluginVersions(rootDir);
const updatedPackage = JSON.parse(
fs.readFileSync(path.join(rootDir, "extensions/bluebubbles/package.json"), "utf8"),
fs.readFileSync(path.join(rootDir, "extensions/imessage/package.json"), "utf8"),
) as {
version?: string;
devDependencies?: Record<string, string>;
@@ -65,7 +65,7 @@ describe("syncPluginVersions", () => {
};
};
expect(summary.updated).toContain("@openclaw/bluebubbles");
expect(summary.updated).toContain("@openclaw/imessage");
expect(updatedPackage.version).toBe("2026.4.1");
expect(updatedPackage.devDependencies?.openclaw).toBe("workspace:*");
expect(updatedPackage.peerDependencies?.openclaw).toBe(">=2026.4.1");

View File

@@ -723,17 +723,6 @@ describe("test-projects args", () => {
]);
});
it("routes bluebubbles extension tests to the bluebubbles config", () => {
expect(buildVitestRunPlans(["extensions/bluebubbles/src/monitor.test.ts"])).toEqual([
{
config: "test/vitest/vitest.extension-bluebubbles.config.ts",
forwardedArgs: [],
includePatterns: ["extensions/bluebubbles/src/monitor.test.ts"],
watchMode: false,
},
]);
});
it("routes feishu extension tests to the feishu config", () => {
expect(buildVitestRunPlans(["extensions/feishu/src/channel.test.ts"])).toEqual([
{

View File

@@ -8,14 +8,14 @@ const { loadPluginManifestRegistryMock } = vi.hoisted(() => ({
const { loadBundledPluginPublicArtifactModuleSyncMock } = vi.hoisted(() => ({
loadBundledPluginPublicArtifactModuleSyncMock: vi.fn(
({ artifactBasename, dirName }: { artifactBasename: string; dirName: string }) => {
if (dirName === "bluebubbles" && artifactBasename === "secret-contract-api.js") {
if (dirName === "discord" && artifactBasename === "secret-contract-api.js") {
return {
collectRuntimeConfigAssignments: () => undefined,
secretTargetRegistryEntries: [
{
id: "channels.bluebubbles.accounts.*.password",
id: "channels.discord.accounts.*.token",
type: "channel",
path: "channels.bluebubbles.accounts.*.password",
path: "channels.discord.accounts.*.token",
},
],
};
@@ -52,17 +52,17 @@ describe("channel contract api explicit fast path", () => {
});
it("resolves bundled channel secret contracts by explicit channel id without manifest scans", () => {
const api = loadBundledChannelSecretContractApi("bluebubbles");
const api = loadBundledChannelSecretContractApi("discord");
expect(api?.collectRuntimeConfigAssignments).toBeTypeOf("function");
expect(loadBundledPluginPublicArtifactModuleSyncMock).toHaveBeenCalledWith({
dirName: "bluebubbles",
dirName: "discord",
artifactBasename: "secret-contract-api.js",
});
expect(api?.secretTargetRegistryEntries).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: "channels.bluebubbles.accounts.*.password",
id: "channels.discord.accounts.*.token",
}),
]),
);

View File

@@ -11,7 +11,7 @@ const CORE_SECRET_SURFACE_GUARDS = [
path: "src/secrets/runtime-config-collectors-channels.ts",
forbiddenPatterns: [
/["']irc["']/,
/["']bluebubbles["']/,
/["']imessage["']/,
/["']msteams["']/,
/["']nextcloud-talk["']/,
],
@@ -20,7 +20,7 @@ const CORE_SECRET_SURFACE_GUARDS = [
path: "src/secrets/target-registry-data.ts",
forbiddenPatterns: [
/channels\.irc\./,
/channels\.bluebubbles\./,
/channels\.imessage\./,
/channels\.msteams\./,
/channels\.nextcloud-talk\./,
/plugins\.entries\.(?:brave|google|exa|xai|moonshot|perplexity|firecrawl|tavily|minimax)\.config\.web(?:Search|Fetch)\.apiKey/,

View File

@@ -31,7 +31,7 @@ describe("runtime channel config collectors", () => {
collectChannelConfigAssignments({
config: {
channels: {
bluebubbles: {
imessage: {
accounts: {
ops: {},
},
@@ -43,7 +43,7 @@ describe("runtime channel config collectors", () => {
});
expect(loadChannelSecretContractApi).toHaveBeenCalledWith({
channelId: "bluebubbles",
channelId: "imessage",
config: expect.any(Object),
env: undefined,
loadablePluginOrigins: undefined,

View File

@@ -39,7 +39,6 @@ import {
const { prepareSecretsRuntimeSnapshot } = setupSecretsRuntimeSnapshotTestHooks();
const EXTERNALIZED_CHANNEL_IDS = [
"bluebubbles",
"discord",
"feishu",
"googlechat",
@@ -128,20 +127,6 @@ describe("secrets runtime externalized channel SecretRef audit", () => {
const records = configureExternalChannelRecords();
const config = asConfig({
channels: {
bluebubbles: {
serverUrl: "http://127.0.0.1:1234",
password: ref("BLUEBUBBLES_PASSWORD"),
accounts: {
inherited: {
enabled: true,
},
work: {
enabled: true,
serverUrl: "http://127.0.0.1:1235",
password: ref("BLUEBUBBLES_WORK_PASSWORD"),
},
},
},
discord: {
token: ref("DISCORD_TOKEN"),
pluralkit: {
@@ -248,8 +233,6 @@ describe("secrets runtime externalized channel SecretRef audit", () => {
const snapshot = await prepareSecretsRuntimeSnapshot({
config,
env: {
BLUEBUBBLES_PASSWORD: "bluebubbles-password",
BLUEBUBBLES_WORK_PASSWORD: "bluebubbles-work-password",
DISCORD_TOKEN: "discord-token",
DISCORD_PLURALKIT_TOKEN: "discord-pluralkit-token",
DISCORD_VOICE_TTS_API_KEY: "discord-voice-tts-api-key",
@@ -279,8 +262,6 @@ describe("secrets runtime externalized channel SecretRef audit", () => {
});
expectResolvedPaths(snapshot.config, {
"channels.bluebubbles.password": "bluebubbles-password",
"channels.bluebubbles.accounts.work.password": "bluebubbles-work-password",
"channels.discord.token": "discord-token",
"channels.discord.pluralkit.token": "discord-pluralkit-token",
"channels.discord.voice.tts.providers.openai.apiKey": "discord-voice-tts-api-key",
@@ -314,16 +295,6 @@ describe("secrets runtime externalized channel SecretRef audit", () => {
const records = configureExternalChannelRecords();
const config = asConfig({
channels: {
bluebubbles: {
enabled: false,
password: inactiveExecRef("BLUEBUBBLES_DISABLED_PASSWORD"),
accounts: {
disabled: {
enabled: false,
password: inactiveExecRef("BLUEBUBBLES_DISABLED_ACCOUNT_PASSWORD"),
},
},
},
discord: {
enabled: false,
token: inactiveExecRef("DISCORD_DISABLED_TOKEN"),
@@ -435,8 +406,6 @@ describe("secrets runtime externalized channel SecretRef audit", () => {
).toEqual(inactiveExecRef("ZALO_DISABLED_ACCOUNT_BOT_TOKEN"));
expect(snapshot.warnings.map((warning) => warning.path)).toEqual(
expect.arrayContaining([
"channels.bluebubbles.password",
"channels.bluebubbles.accounts.disabled.password",
"channels.discord.token",
"channels.discord.pluralkit.token",
"channels.discord.voice.tts.providers.openai.apiKey",

View File

@@ -342,7 +342,7 @@ describe("security/dm-policy-shared", () => {
});
const channels = [
"bluebubbles",
"imessage",
"imessage",
"signal",
"telegram",

View File

@@ -12,7 +12,7 @@ const ENVELOPE_CHANNELS = [
"Matrix",
"Zalo",
"Zalo Personal",
"BlueBubbles",
"iMessage",
];
const MESSAGE_ID_LINE = /^\s*\[message_id:\s*[^\]]+\]\s*$/i;

View File

@@ -92,12 +92,6 @@ describe("bundled plugin build entries", () => {
const entries = listBundledPluginBuildEntries();
const artifacts = listBundledPluginPackArtifacts();
expect(Object.keys(entries).some((entry) => entry.startsWith("extensions/bluebubbles/"))).toBe(
true,
);
expect(artifacts.some((artifact) => artifact.startsWith("dist/extensions/bluebubbles/"))).toBe(
false,
);
for (const pluginId of ["acpx", "googlechat", "line"]) {
expect(
Object.keys(entries).some((entry) => entry.startsWith(`extensions/${pluginId}/`)),

View File

@@ -47,15 +47,6 @@ describe("scripts/test-extension.mjs", () => {
expect(plan.hasTests).toBe(true);
});
it("resolves bluebubbles onto the bluebubbles vitest config", () => {
const plan = resolveExtensionTestPlan({ targetArg: "bluebubbles", cwd: process.cwd() });
expect(plan.extensionId).toBe("bluebubbles");
expect(plan.config).toBe("test/vitest/vitest.extension-bluebubbles.config.ts");
expect(plan.roots).toContain(bundledPluginRoot("bluebubbles"));
expect(plan.hasTests).toBe(true);
});
it("resolves acpx onto the acpx vitest config", () => {
const plan = resolveExtensionTestPlan({ targetArg: "acpx", cwd: process.cwd() });
@@ -272,7 +263,6 @@ describe("scripts/test-extension.mjs", () => {
"msteams",
"feishu",
"irc",
"bluebubbles",
"acpx",
"diffs",
"browser",
@@ -283,7 +273,6 @@ describe("scripts/test-extension.mjs", () => {
expect(batch.extensionIds).toEqual([
"acpx",
"bluebubbles",
"browser",
"diffs",
"feishu",
@@ -312,13 +301,6 @@ describe("scripts/test-extension.mjs", () => {
roots: [bundledPluginRoot("acpx")],
testFileCount: expect.any(Number),
},
{
config: "test/vitest/vitest.extension-bluebubbles.config.ts",
estimatedCost: expect.any(Number),
extensionIds: ["bluebubbles"],
roots: [bundledPluginRoot("bluebubbles")],
testFileCount: expect.any(Number),
},
{
config: "test/vitest/vitest.extension-browser.config.ts",
estimatedCost: expect.any(Number),

View File

@@ -1227,7 +1227,6 @@ describe("scripts/test-projects full-suite sharding", () => {
"test/vitest/vitest.auto-reply-top-level.config.ts",
"test/vitest/vitest.auto-reply-reply.config.ts",
"test/vitest/vitest.extension-acpx.config.ts",
"test/vitest/vitest.extension-bluebubbles.config.ts",
"test/vitest/vitest.extension-diffs.config.ts",
"test/vitest/vitest.extension-discord.config.ts",
"test/vitest/vitest.extension-feishu.config.ts",

View File

@@ -19,7 +19,6 @@ import { createCommandsVitestConfig } from "./vitest/vitest.commands.config.ts";
import { createCronVitestConfig } from "./vitest/vitest.cron.config.ts";
import { createDaemonVitestConfig } from "./vitest/vitest.daemon.config.ts";
import { createExtensionAcpxVitestConfig } from "./vitest/vitest.extension-acpx.config.ts";
import { createExtensionBlueBubblesVitestConfig } from "./vitest/vitest.extension-bluebubbles.config.ts";
import { createExtensionBrowserVitestConfig } from "./vitest/vitest.extension-browser.config.ts";
import { createExtensionChannelsVitestConfig } from "./vitest/vitest.extension-channels.config.ts";
import { createExtensionDiffsVitestConfig } from "./vitest/vitest.extension-diffs.config.ts";
@@ -236,7 +235,6 @@ describe("scoped vitest configs", () => {
const defaultCliConfig = createCliVitestConfig({});
const defaultExtensionsConfig = createExtensionsVitestConfig({});
const defaultExtensionAcpxConfig = createExtensionAcpxVitestConfig({});
const defaultExtensionBlueBubblesConfig = createExtensionBlueBubblesVitestConfig({});
const defaultExtensionChannelsConfig = createExtensionChannelsVitestConfig({});
const defaultExtensionBrowserConfig = createExtensionBrowserVitestConfig({});
const defaultExtensionDiffsConfig = createExtensionDiffsVitestConfig({});
@@ -432,13 +430,6 @@ describe("scoped vitest configs", () => {
}
});
it("normalizes bluebubbles extension include patterns relative to the scoped dir", () => {
expect(defaultExtensionBlueBubblesConfig.test?.dir).toBe(
path.join(process.cwd(), "extensions"),
);
expect(defaultExtensionBlueBubblesConfig.test?.include).toEqual(["bluebubbles/**/*.test.ts"]);
});
it("normalizes acpx extension include patterns relative to the scoped dir", () => {
expect(defaultExtensionAcpxConfig.test?.dir).toBe(path.join(process.cwd(), "extensions"));
expect(defaultExtensionAcpxConfig.test?.include).toEqual(["acpx/**/*.test.ts"]);
@@ -625,15 +616,6 @@ describe("scoped vitest configs", () => {
).toBe(true);
});
it("keeps bluebubbles tests out of the shared extensions lane", () => {
const extensionExcludes = defaultExtensionsConfig.test?.exclude ?? [];
expect(
extensionExcludes.some((pattern) =>
path.matchesGlob("bluebubbles/src/monitor.test.ts", pattern),
),
).toBe(true);
});
it("keeps feishu tests out of the shared extensions lane", () => {
const extensionExcludes = defaultExtensionsConfig.test?.exclude ?? [];
expect(

View File

@@ -55,7 +55,6 @@ export const rootVitestProjects = [
"test/vitest/vitest.wizard.config.ts",
"test/vitest/vitest.channels.config.ts",
"test/vitest/vitest.extension-acpx.config.ts",
"test/vitest/vitest.extension-bluebubbles.config.ts",
"test/vitest/vitest.extension-diffs.config.ts",
"test/vitest/vitest.extension-discord.config.ts",
"test/vitest/vitest.extension-feishu.config.ts",

View File

@@ -1,7 +1,6 @@
import { BUNDLED_PLUGIN_TEST_GLOB } from "./vitest.bundled-plugin-paths.ts";
import { extensionExcludedChannelTestGlobs } from "./vitest.channel-paths.mjs";
import { acpxExtensionTestRoots } from "./vitest.extension-acpx-paths.mjs";
import { blueBubblesExtensionTestRoots } from "./vitest.extension-bluebubbles-paths.mjs";
import { browserExtensionTestRoots } from "./vitest.extension-browser-paths.mjs";
import { diffsExtensionTestRoots } from "./vitest.extension-diffs-paths.mjs";
import { feishuExtensionTestRoots } from "./vitest.extension-feishu-paths.mjs";
@@ -45,7 +44,6 @@ export function createExtensionsVitestConfig(
exclude: [
...extensionExcludedChannelTestGlobs,
...acpxExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`),
...blueBubblesExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`),
...browserExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`),
...diffsExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`),
...feishuExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`),

View File

@@ -62,7 +62,6 @@ const SCOPED_PROJECT_GROUP_ORDER_BY_NAME = new Map(
"cron",
"daemon",
"extension-acpx",
"extension-bluebubbles",
"extension-channels",
"extension-diffs",
"extension-discord",

View File

@@ -206,8 +206,6 @@ export const sharedVitestConfig = {
"test/vitest/vitest.e2e.config.ts",
"test/vitest/vitest.extension-acpx-paths.mjs",
"test/vitest/vitest.extension-acpx.config.ts",
"test/vitest/vitest.extension-bluebubbles-paths.mjs",
"test/vitest/vitest.extension-bluebubbles.config.ts",
"test/vitest/vitest.extension-channel-single-config.ts",
"test/vitest/vitest.extension-channel-split-paths.mjs",
"test/vitest/vitest.extension-channels.config.ts",

View File

@@ -111,7 +111,6 @@ export const fullSuiteVitestShards = [
name: "extensions",
projects: [
"test/vitest/vitest.extension-acpx.config.ts",
"test/vitest/vitest.extension-bluebubbles.config.ts",
"test/vitest/vitest.extension-diffs.config.ts",
"test/vitest/vitest.extension-discord.config.ts",
"test/vitest/vitest.extension-feishu.config.ts",

View File

@@ -190,7 +190,7 @@ describe("parseSessionKey", () => {
});
it("identifies direct chat with known channel", () => {
expect(parseSessionKey("agent:main:bluebubbles:direct:+19257864429")).toEqual({
expect(parseSessionKey("agent:main:imessage:direct:+19257864429")).toEqual({
prefix: "",
fallbackName: "iMessage · +19257864429",
});
@@ -218,7 +218,7 @@ describe("parseSessionKey", () => {
});
it("identifies channel-prefixed legacy keys", () => {
expect(parseSessionKey("bluebubbles:g-agent-main-bluebubbles-direct-+19257864429")).toEqual({
expect(parseSessionKey("imessage:g-agent-main-imessage-direct-+19257864429")).toEqual({
prefix: "",
fallbackName: "iMessage Session",
});
@@ -309,7 +309,7 @@ describe("resolveSessionDisplayName", () => {
});
it("parses direct chat key with channel", () => {
expect(resolveSessionDisplayName("agent:main:bluebubbles:direct:+19257864429")).toBe(
expect(resolveSessionDisplayName("agent:main:imessage:direct:+19257864429")).toBe(
"iMessage · +19257864429",
);
});
@@ -448,8 +448,8 @@ describe("resolveSessionDisplayName", () => {
it("does not prefix non-typed sessions with labels", () => {
expect(
resolveSessionDisplayName(
"agent:main:bluebubbles:direct:+19257864429",
row({ key: "agent:main:bluebubbles:direct:+19257864429", label: "Tyler" }),
"agent:main:imessage:direct:+19257864429",
row({ key: "agent:main:imessage:direct:+19257864429", label: "Tyler" }),
),
).toBe("Tyler");
});

View File

@@ -407,7 +407,7 @@ async function switchChatThinkingLevel(state: AppViewState, nextThinkingLevel: s
/* Channel display labels. */
const CHANNEL_LABELS: Record<string, string> = {
bluebubbles: "iMessage",
imessage: "iMessage",
telegram: "Telegram",
discord: "Discord",
signal: "Signal",
@@ -471,7 +471,7 @@ export function parseSessionKey(key: string): SessionKeyInfo {
return { prefix: "", fallbackName: `${channelLabel} Group` };
}
// Channel-prefixed legacy keys, for example "bluebubbles:g-...".
// Channel-prefixed legacy keys, for example "imessage:g-...".
for (const ch of KNOWN_CHANNEL_KEYS) {
if (key === ch || key.startsWith(`${ch}:`)) {
return { prefix: "", fallbackName: `${CHANNEL_LABELS[ch]} Session` };