From e8f18f95d5da89d2fda288a538aff8886f7e400d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 22 Apr 2026 03:36:06 +0100 Subject: [PATCH] test(docker): cover slack bundled runtime deps --- .../bundled-channel-runtime-deps-docker.sh | 37 ++++++++++- ...doctor-bundled-plugin-runtime-deps.test.ts | 64 ++++++++++++++++++- .../doctor-bundled-plugin-runtime-deps.ts | 15 +++-- src/plugins/bundled-runtime-deps.ts | 7 +- 4 files changed, 113 insertions(+), 10 deletions(-) diff --git a/scripts/e2e/bundled-channel-runtime-deps-docker.sh b/scripts/e2e/bundled-channel-runtime-deps-docker.sh index 6a6b763fdfa..48ee70c2ecc 100644 --- a/scripts/e2e/bundled-channel-runtime-deps-docker.sh +++ b/scripts/e2e/bundled-channel-runtime-deps-docker.sh @@ -60,6 +60,7 @@ command -v openclaw >/dev/null package_root="$(npm root -g)/openclaw" test -d "$package_root/dist/extensions/telegram" test -d "$package_root/dist/extensions/discord" +test -d "$package_root/dist/extensions/slack" if [ -d "$package_root/dist/extensions/telegram/node_modules" ]; then echo "telegram runtime deps should not be preinstalled in package" >&2 @@ -71,6 +72,11 @@ if [ -d "$package_root/dist/extensions/discord/node_modules" ]; then find "$package_root/dist/extensions/discord/node_modules" -maxdepth 2 -type f | head -20 >&2 || true exit 1 fi +if [ -d "$package_root/dist/extensions/slack/node_modules" ]; then + echo "slack runtime deps should not be preinstalled in package" >&2 + find "$package_root/dist/extensions/slack/node_modules" -maxdepth 2 -type f | head -20 >&2 || true + exit 1 +fi write_config() { local mode="$1" @@ -138,6 +144,15 @@ if (mode === "discord") { }, }; } +if (mode === "slack") { + config.channels = { + ...(config.channels || {}), + slack: { + ...(config.channels?.slack || {}), + enabled: true, + }, + }; +} fs.mkdirSync(path.dirname(configPath), { recursive: true }); fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8"); @@ -392,6 +407,12 @@ config.channels = { dmPolicy: "disabled", groupPolicy: "disabled", }, + slack: { + ...(config.channels?.slack || {}), + enabled: mode === "slack", + botToken: "xoxb-bundled-channel-update-token", + appToken: "xapp-bundled-channel-update-token", + }, }; fs.mkdirSync(path.dirname(configPath), { recursive: true }); @@ -542,7 +563,11 @@ assert_dep_available telegram grammy echo "Mutating installed package: remove Telegram deps, then update-mode doctor repairs them..." remove_runtime_dep telegram grammy assert_no_dep_available telegram grammy -OPENCLAW_UPDATE_IN_PROGRESS=1 openclaw doctor --non-interactive >/tmp/openclaw-update-mode-doctor.log 2>&1 +if ! OPENCLAW_UPDATE_IN_PROGRESS=1 openclaw doctor --non-interactive >/tmp/openclaw-update-mode-doctor.log 2>&1; then + echo "update-mode doctor failed while repairing Telegram deps" >&2 + cat /tmp/openclaw-update-mode-doctor.log >&2 + exit 1 +fi assert_dep_available telegram grammy echo "Mutating config to Discord and rerunning same-version update path..." @@ -554,6 +579,15 @@ cat /tmp/openclaw-update-discord.json assert_update_ok /tmp/openclaw-update-discord.json "$candidate_version" assert_dep_available discord discord-api-types +echo "Mutating config to Slack and rerunning same-version update path..." +write_config slack +remove_runtime_dep slack @slack/web-api +assert_no_dep_available slack @slack/web-api +run_update_and_capture slack /tmp/openclaw-update-slack.json +cat /tmp/openclaw-update-slack.json +assert_update_ok /tmp/openclaw-update-slack.json "$candidate_version" +assert_dep_available slack @slack/web-api + echo "bundled channel runtime deps Docker update E2E passed" EOF then @@ -568,6 +602,7 @@ EOF run_channel_scenario telegram grammy run_channel_scenario discord discord-api-types +run_channel_scenario slack @slack/web-api if [ "$RUN_UPDATE_SCENARIO" != "0" ]; then run_update_scenario fi diff --git a/src/commands/doctor-bundled-plugin-runtime-deps.test.ts b/src/commands/doctor-bundled-plugin-runtime-deps.test.ts index 7b729e5b6d6..040858fe413 100644 --- a/src/commands/doctor-bundled-plugin-runtime-deps.test.ts +++ b/src/commands/doctor-bundled-plugin-runtime-deps.test.ts @@ -172,7 +172,11 @@ describe("doctor bundled plugin runtime deps", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-bundled-")); writeJson(path.join(root, "package.json"), { name: "openclaw" }); writeBundledChannelPlugin(root, "telegram", { grammy: "1.37.0" }); - const installed: Array<{ installRoot: string; missingSpecs: string[] }> = []; + const installed: Array<{ + installRoot: string; + missingSpecs: string[]; + installSpecs: string[]; + }> = []; const prompter = { shouldRepair: false, shouldForce: false, @@ -207,6 +211,64 @@ describe("doctor bundled plugin runtime deps", () => { { installRoot: root, missingSpecs: ["grammy@1.37.0"], + installSpecs: ["grammy@1.37.0"], + }, + ]); + }); + + it("retains configured bundled deps when repairing a subset", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-bundled-")); + writeJson(path.join(root, "package.json"), { name: "openclaw" }); + writeBundledChannelPlugin(root, "telegram", { grammy: "1.37.0" }); + writeBundledChannelPlugin(root, "slack", { "@slack/web-api": "7.15.1" }); + writeJson(path.join(root, "node_modules", "@slack", "web-api", "package.json"), { + name: "@slack/web-api", + version: "7.15.1", + }); + const installed: Array<{ + installRoot: string; + missingSpecs: string[]; + installSpecs: string[]; + }> = []; + const prompter = { + shouldRepair: false, + shouldForce: false, + repairMode: { + shouldRepair: false, + shouldForce: false, + nonInteractive: true, + canPrompt: false, + updateInProgress: false, + }, + confirm: async () => false, + confirmAutoFix: async () => false, + confirmAggressiveAutoFix: async () => false, + confirmRuntimeRepair: async () => false, + select: async (_params: unknown, fallback: unknown) => fallback, + } as DoctorPrompter; + + await maybeRepairBundledPluginRuntimeDeps({ + runtime: { error: () => {} } as never, + prompter, + packageRoot: root, + includeConfiguredChannels: true, + config: { + plugins: { enabled: true }, + channels: { + telegram: { enabled: true }, + slack: { enabled: false, botToken: "xoxb-test", appToken: "xapp-test" }, + }, + }, + installDeps: (params) => { + installed.push(params); + }, + }); + + expect(installed).toEqual([ + { + installRoot: root, + missingSpecs: ["grammy@1.37.0"], + installSpecs: ["@slack/web-api@7.15.1", "grammy@1.37.0"], }, ]); }); diff --git a/src/commands/doctor-bundled-plugin-runtime-deps.ts b/src/commands/doctor-bundled-plugin-runtime-deps.ts index ea72d7ed8d1..42baa45180b 100644 --- a/src/commands/doctor-bundled-plugin-runtime-deps.ts +++ b/src/commands/doctor-bundled-plugin-runtime-deps.ts @@ -16,7 +16,11 @@ export async function maybeRepairBundledPluginRuntimeDeps(params: { env?: NodeJS.ProcessEnv; packageRoot?: string | null; includeConfiguredChannels?: boolean; - installDeps?: (params: { installRoot: string; missingSpecs: string[] }) => void; + installDeps?: (params: { + installRoot: string; + missingSpecs: string[]; + installSpecs: string[]; + }) => void; }): Promise { const packageRoot = params.packageRoot ?? @@ -29,7 +33,7 @@ export async function maybeRepairBundledPluginRuntimeDeps(params: { return; } - const { missing, conflicts } = scanBundledPluginRuntimeDeps({ + const { deps, missing, conflicts } = scanBundledPluginRuntimeDeps({ packageRoot, config: params.config, includeConfiguredChannels: params.includeConfiguredChannels, @@ -58,6 +62,7 @@ export async function maybeRepairBundledPluginRuntimeDeps(params: { } const missingSpecs = missing.map((dep) => `${dep.name}@${dep.version}`); + const installSpecs = deps.map((dep) => `${dep.name}@${dep.version}`); note( [ "Bundled plugin runtime deps are missing.", @@ -84,11 +89,11 @@ export async function maybeRepairBundledPluginRuntimeDeps(params: { ((installParams) => installBundledRuntimeDeps({ installRoot: installParams.installRoot, - missingSpecs: installParams.missingSpecs, + missingSpecs: installParams.installSpecs, env: params.env ?? process.env, })); - install({ installRoot: packageRoot, missingSpecs }); - note(`Installed bundled plugin deps: ${missingSpecs.join(", ")}`, "Bundled plugins"); + install({ installRoot: packageRoot, missingSpecs, installSpecs }); + note(`Installed bundled plugin deps: ${installSpecs.join(", ")}`, "Bundled plugins"); } catch (error) { params.runtime.error(`Failed to install bundled plugin runtime deps: ${String(error)}`); } diff --git a/src/plugins/bundled-runtime-deps.ts b/src/plugins/bundled-runtime-deps.ts index c4c800b13de..25294cb3c18 100644 --- a/src/plugins/bundled-runtime-deps.ts +++ b/src/plugins/bundled-runtime-deps.ts @@ -484,15 +484,16 @@ export function scanBundledPluginRuntimeDeps(params: { pluginIds?: readonly string[]; includeConfiguredChannels?: boolean; }): { + deps: RuntimeDepEntry[]; missing: RuntimeDepEntry[]; conflicts: RuntimeDepConflict[]; } { if (isSourceCheckoutRoot(params.packageRoot)) { - return { missing: [], conflicts: [] }; + return { deps: [], missing: [], conflicts: [] }; } const extensionsDir = path.join(params.packageRoot, "dist", "extensions"); if (!fs.existsSync(extensionsDir)) { - return { missing: [], conflicts: [] }; + return { deps: [], missing: [], conflicts: [] }; } const { deps, conflicts } = collectBundledPluginRuntimeDeps({ extensionsDir, @@ -509,7 +510,7 @@ export function scanBundledPluginRuntimeDeps(params: { !fs.existsSync(path.join(extensionsDir, pluginId, dependencySentinelPath(dep.name))), ), ); - return { missing, conflicts }; + return { deps, missing, conflicts }; } export function resolveBundledRuntimeDependencyInstallRoot(pluginRoot: string): string {