From 5a5aa3a17828dcadd6c3cc3ee70459db1ca12ee6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 23 Apr 2026 00:50:34 +0100 Subject: [PATCH] fix(config): tolerate missing channel metadata during auto-enable --- CHANGELOG.md | 1 + .../plugin-auto-enable.prefer-over.test.ts | 89 +++++++++++++++++++ src/config/plugin-auto-enable.prefer-over.ts | 2 +- src/config/plugin-auto-enable.shared.ts | 2 +- 4 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 src/config/plugin-auto-enable.prefer-over.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 627e62542f5..5bb964d73a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai - Agents/MCP: keep `mcp.servers` and bundle MCP tools available in Pi embedded `coding` and `messaging` sessions while preserving `minimal` profile and `tools.deny: ["bundle-mcp"]` opt-out behavior. Fixes #68875 and #68818. +- Plugins/startup: tolerate transient bundled-channel catalog/metadata drift while auto-enabling configured plugins, so CLI and gateway startup no longer crash when a channel id is known but its display metadata is unavailable. - CLI/Claude: report CLI-backed reply runs as streaming while Claude/Codex CLI turns are still in flight, so WebChat keeps visible response state until the backend finishes. Fixes #70125. - Codex harness: rotate the shared app-server websocket client when the configured bearer token changes, so auth-token refreshes reconnect with the new `Authorization` header instead of reusing a stale socket. (#70328) Thanks @Lucenx9. - Telegram/sandbox: keep Telegram bot DMs on per-account sender session keys even when `session.dmScope=main`, so sandbox/tool policy can distinguish Telegram-originated direct chats from the agent main session. diff --git a/src/config/plugin-auto-enable.prefer-over.test.ts b/src/config/plugin-auto-enable.prefer-over.test.ts new file mode 100644 index 00000000000..cbb282c83b1 --- /dev/null +++ b/src/config/plugin-auto-enable.prefer-over.test.ts @@ -0,0 +1,89 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { PluginManifestRegistry } from "../plugins/manifest-registry.js"; +import { cleanupTrackedTempDirs } from "../plugins/test-helpers/fs-fixtures.js"; + +const tempDirs: string[] = []; + +function makeTempDir(): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-prefer-over-")); + tempDirs.push(dir); + return dir; +} + +function writeBundledChannelPackage(rootDir: string, channelId: string): void { + const pluginDir = path.join(rootDir, channelId); + fs.mkdirSync(pluginDir, { recursive: true }); + fs.writeFileSync( + path.join(pluginDir, "package.json"), + JSON.stringify({ + openclaw: { + channel: { + id: channelId, + label: "Cache Drift", + selectionLabel: "Cache Drift", + docsPath: `/channels/${channelId}`, + blurb: "Cache drift fixture", + }, + }, + }), + "utf-8", + ); +} + +const EMPTY_MANIFEST_REGISTRY: PluginManifestRegistry = { + plugins: [], + diagnostics: [], +}; + +afterEach(() => { + vi.unstubAllEnvs(); + vi.resetModules(); + cleanupTrackedTempDirs(tempDirs); +}); + +describe("plugin auto-enable preferOver", () => { + it("tolerates bundled channel id metadata drift during auto-enable", async () => { + vi.resetModules(); + const rootDir = makeTempDir(); + const channelId = "cache-drift-channel"; + writeBundledChannelPackage(rootDir, channelId); + + vi.stubEnv("OPENCLAW_BUNDLED_PLUGINS_DIR", rootDir); + const { normalizeChatChannelId } = await import("../channels/ids.js"); + expect(normalizeChatChannelId(channelId)).toBe(channelId); + + vi.stubEnv("OPENCLAW_BUNDLED_PLUGINS_DIR", path.join(rootDir, "missing")); + const { materializePluginAutoEnableCandidates } = await import("./plugin-auto-enable.js"); + + const result = materializePluginAutoEnableCandidates({ + config: { + channels: { + [channelId]: { token: "configured" }, + fallback: { token: "configured" }, + }, + }, + candidates: [ + { + pluginId: channelId, + kind: "channel-configured", + channelId, + }, + { + pluginId: "fallback", + kind: "channel-configured", + channelId: "fallback", + }, + ], + env: { + OPENCLAW_STATE_DIR: path.join(rootDir, "state"), + OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(rootDir, "missing"), + }, + manifestRegistry: EMPTY_MANIFEST_REGISTRY, + }); + + expect(result.config.channels?.[channelId]?.enabled).toBe(true); + }); +}); diff --git a/src/config/plugin-auto-enable.prefer-over.ts b/src/config/plugin-auto-enable.prefer-over.ts index bc021139b76..0466db63461 100644 --- a/src/config/plugin-auto-enable.prefer-over.ts +++ b/src/config/plugin-auto-enable.prefer-over.ts @@ -96,7 +96,7 @@ function resolveBuiltInChannelPreferOver(channelId: string): readonly string[] { if (!builtInChannelId) { return []; } - return getChatChannelMeta(builtInChannelId).preferOver ?? []; + return getChatChannelMeta(builtInChannelId)?.preferOver ?? []; } function resolvePreferredOverIds( diff --git a/src/config/plugin-auto-enable.shared.ts b/src/config/plugin-auto-enable.shared.ts index de80d253190..9217b3a8256 100644 --- a/src/config/plugin-auto-enable.shared.ts +++ b/src/config/plugin-auto-enable.shared.ts @@ -684,7 +684,7 @@ function resolveChannelAutoEnableDisplayLabel( ): string | undefined { const builtInChannelId = normalizeChatChannelId(entry.channelId); if (builtInChannelId) { - return getChatChannelMeta(builtInChannelId).label; + return getChatChannelMeta(builtInChannelId)?.label; } const plugin = manifestRegistry.plugins.find((record) => record.id === entry.pluginId); return plugin?.channelConfigs?.[entry.channelId]?.label ?? plugin?.channelCatalogMeta?.label;