fix(slack): harden bolt import interop (#45953)

* fix(slack): harden bolt import interop

* fix(slack): simplify bolt interop resolver

* fix(slack): harden startup bolt interop

* fix(slack): place changelog entry at section end

---------

Co-authored-by: Ubuntu <ubuntu@vps-1c82b947.vps.ovh.net>
Co-authored-by: Altay <altay@uinaf.dev>
This commit is contained in:
Yauheni Shauchenka
2026-03-16 15:49:24 +03:00
committed by GitHub
parent 7d4ccee717
commit 80bef826f8
3 changed files with 167 additions and 8 deletions

View File

@@ -93,6 +93,7 @@ Docs: https://docs.openclaw.ai
- Nodes/pending actions: re-check queued foreground actions against the current node command policy before returning them to the node. (#46815) Thanks @zpbrent and @vincentkoc.
- Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46411)
- CLI/completion: reduce recursive completion-script string churn and fix nested PowerShell command-path matching so generated nested completions resolve on PowerShell too. (#45537) Thanks @yiShanXin and @vincentkoc.
- Slack/startup: harden `@slack/bolt` import interop across current bundled runtime shapes so Slack monitors no longer crash with `App is not a constructor` after plugin-sdk bundling changes. (#45953) thanks @merc1305.
## 2026.3.13

View File

@@ -0,0 +1,94 @@
import { describe, expect, it } from "vitest";
import { __testing } from "./provider.js";
describe("resolveSlackBoltInterop", () => {
class FakeApp {}
class FakeHTTPReceiver {}
it("uses the default import when it already exposes named exports", () => {
const resolved = __testing.resolveSlackBoltInterop({
defaultImport: {
App: FakeApp,
HTTPReceiver: FakeHTTPReceiver,
},
namespaceImport: {},
});
expect(resolved).toEqual({
App: FakeApp,
HTTPReceiver: FakeHTTPReceiver,
});
});
it("uses nested default export when the default import is a wrapper object", () => {
const resolved = __testing.resolveSlackBoltInterop({
defaultImport: {
default: {
App: FakeApp,
HTTPReceiver: FakeHTTPReceiver,
},
},
namespaceImport: {},
});
expect(resolved).toEqual({
App: FakeApp,
HTTPReceiver: FakeHTTPReceiver,
});
});
it("uses the namespace receiver when the default import is the App constructor itself", () => {
const resolved = __testing.resolveSlackBoltInterop({
defaultImport: FakeApp,
namespaceImport: {
HTTPReceiver: FakeHTTPReceiver,
},
});
expect(resolved).toEqual({
App: FakeApp,
HTTPReceiver: FakeHTTPReceiver,
});
});
it("uses namespace.default when it exposes named exports", () => {
const resolved = __testing.resolveSlackBoltInterop({
defaultImport: undefined,
namespaceImport: {
default: {
App: FakeApp,
HTTPReceiver: FakeHTTPReceiver,
},
},
});
expect(resolved).toEqual({
App: FakeApp,
HTTPReceiver: FakeHTTPReceiver,
});
});
it("falls back to the namespace import when it exposes named exports", () => {
const resolved = __testing.resolveSlackBoltInterop({
defaultImport: undefined,
namespaceImport: {
App: FakeApp,
HTTPReceiver: FakeHTTPReceiver,
},
});
expect(resolved).toEqual({
App: FakeApp,
HTTPReceiver: FakeHTTPReceiver,
});
});
it("throws when the module cannot be resolved", () => {
expect(() =>
__testing.resolveSlackBoltInterop({
defaultImport: null,
namespaceImport: {},
}),
).toThrow("Unable to resolve @slack/bolt App/HTTPReceiver exports");
});
});

View File

@@ -1,5 +1,5 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import SlackBolt from "@slack/bolt";
import SlackBolt, * as SlackBoltNamespace from "@slack/bolt";
import { resolveTextChunkLimit } from "../../../../src/auto-reply/chunk.js";
import { DEFAULT_GROUP_HISTORY_LIMIT } from "../../../../src/auto-reply/reply/history.js";
import {
@@ -46,14 +46,77 @@ import {
import { registerSlackMonitorSlashCommands } from "./slash.js";
import type { MonitorSlackOpts } from "./types.js";
const slackBoltModule = SlackBolt as typeof import("@slack/bolt") & {
default?: typeof import("@slack/bolt");
type SlackAppConstructor = typeof import("@slack/bolt").App;
type SlackHttpReceiverConstructor = typeof import("@slack/bolt").HTTPReceiver;
type SlackBoltResolvedExports = {
App: SlackAppConstructor;
HTTPReceiver: SlackHttpReceiverConstructor;
};
// Bun allows named imports from CJS; Node ESM doesn't. Use default+fallback for compatibility.
// Fix: Check if module has App property directly (Node 25.x ESM/CJS compat issue)
const slackBolt =
(slackBoltModule.App ? slackBoltModule : slackBoltModule.default) ?? slackBoltModule;
const { App, HTTPReceiver } = slackBolt;
type Constructor = abstract new (...args: never[]) => unknown;
function isConstructorFunction<T extends Constructor>(value: unknown): value is T {
return typeof value === "function";
}
function resolveSlackBoltModule(value: unknown): SlackBoltResolvedExports | null {
if (!value || typeof value !== "object") {
return null;
}
const app = Reflect.get(value, "App");
const httpReceiver = Reflect.get(value, "HTTPReceiver");
if (
!isConstructorFunction<SlackAppConstructor>(app) ||
!isConstructorFunction<SlackHttpReceiverConstructor>(httpReceiver)
) {
return null;
}
return {
App: app,
HTTPReceiver: httpReceiver,
};
}
function resolveSlackBoltInterop(params: {
defaultImport: unknown;
namespaceImport: unknown;
}): SlackBoltResolvedExports {
const { defaultImport, namespaceImport } = params;
const nestedDefault =
defaultImport && typeof defaultImport === "object"
? Reflect.get(defaultImport, "default")
: undefined;
const namespaceDefault =
namespaceImport && typeof namespaceImport === "object"
? Reflect.get(namespaceImport, "default")
: undefined;
const namespaceReceiver =
namespaceImport && typeof namespaceImport === "object"
? Reflect.get(namespaceImport, "HTTPReceiver")
: undefined;
const directModule =
resolveSlackBoltModule(defaultImport) ??
resolveSlackBoltModule(nestedDefault) ??
resolveSlackBoltModule(namespaceDefault) ??
resolveSlackBoltModule(namespaceImport);
if (directModule) {
return directModule;
}
if (
isConstructorFunction<SlackAppConstructor>(defaultImport) &&
isConstructorFunction<SlackHttpReceiverConstructor>(namespaceReceiver)
) {
return {
App: defaultImport,
HTTPReceiver: namespaceReceiver,
};
}
throw new TypeError("Unable to resolve @slack/bolt App/HTTPReceiver exports");
}
const { App, HTTPReceiver } = resolveSlackBoltInterop({
defaultImport: SlackBolt,
namespaceImport: SlackBoltNamespace,
});
const SLACK_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
const SLACK_WEBHOOK_BODY_TIMEOUT_MS = 30_000;
@@ -515,6 +578,7 @@ export const __testing = {
publishSlackDisconnectedStatus,
resolveSlackRuntimeGroupPolicy: resolveOpenProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
resolveSlackBoltInterop,
getSocketEmitter,
waitForSlackSocketDisconnect,
};