mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:20:43 +00:00
fix(channels): thread runtime config through sends
This commit is contained in:
@@ -109,19 +109,14 @@ describe("resolveSlackAccount allowFrom precedence", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveSlackAccount tolerateUnresolvedSecrets", () => {
|
||||
// The static `SlackAccountConfig.botToken` type is `string` because it
|
||||
// models the post-resolution shape, but the runtime cfg snapshot can still
|
||||
// hold an unresolved `SecretRef` object for inactive channel targets (per
|
||||
// the inspect/strict separation in #66818). Cast via `unknown` so the test
|
||||
// can construct that runtime-only shape without weakening the production
|
||||
// type. See #68237.
|
||||
describe("resolveSlackAccount active secret surfaces", () => {
|
||||
const secretRef = { source: "exec", provider: "default", id: "slack_token" } as const;
|
||||
const cfgWithUnresolvedBotTokenRef = {
|
||||
channels: {
|
||||
slack: {
|
||||
accounts: {
|
||||
default: {
|
||||
botToken: { source: "exec", provider: "default", id: "slack_bot_token" },
|
||||
botToken: secretRef,
|
||||
allowFrom: ["U999"],
|
||||
},
|
||||
},
|
||||
@@ -129,7 +124,7 @@ describe("resolveSlackAccount tolerateUnresolvedSecrets", () => {
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
it("throws by default when the snapshot still holds an unresolved SecretRef botToken", () => {
|
||||
it("throws when an enabled account still has an unresolved active bot token SecretRef", () => {
|
||||
expect(() =>
|
||||
resolveSlackAccount({
|
||||
cfg: cfgWithUnresolvedBotTokenRef,
|
||||
@@ -138,100 +133,83 @@ describe("resolveSlackAccount tolerateUnresolvedSecrets", () => {
|
||||
).toThrowError(/channels\.slack\.accounts\.default\.botToken/);
|
||||
});
|
||||
|
||||
it("returns undefined credentials without throwing when tolerateUnresolvedSecrets is set", () => {
|
||||
const resolved = resolveSlackAccount({
|
||||
cfg: cfgWithUnresolvedBotTokenRef,
|
||||
accountId: "default",
|
||||
tolerateUnresolvedSecrets: true,
|
||||
});
|
||||
|
||||
expect(resolved.botToken).toBeUndefined();
|
||||
expect(resolved.botTokenSource).toBe("none");
|
||||
// Surrounding account info still resolves so callers with an explicit
|
||||
// override (for example sendMessageSlack receiving opts.token) can keep
|
||||
// operating.
|
||||
expect(resolved.accountId).toBe("default");
|
||||
expect(resolved.config.allowFrom).toEqual(["U999"]);
|
||||
});
|
||||
|
||||
it("still returns resolved string credentials in tolerant mode", () => {
|
||||
it("does not read credentials for disabled accounts", () => {
|
||||
const resolved = resolveSlackAccount({
|
||||
cfg: {
|
||||
channels: {
|
||||
slack: {
|
||||
accounts: {
|
||||
default: { botToken: "xoxb-resolved", appToken: "xapp-resolved" },
|
||||
default: {
|
||||
enabled: false,
|
||||
botToken: secretRef,
|
||||
appToken: secretRef,
|
||||
userToken: secretRef,
|
||||
allowFrom: ["U999"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig,
|
||||
accountId: "default",
|
||||
});
|
||||
|
||||
expect(resolved.botToken).toBeUndefined();
|
||||
expect(resolved.botTokenSource).toBe("none");
|
||||
expect(resolved.appToken).toBeUndefined();
|
||||
expect(resolved.appTokenSource).toBe("none");
|
||||
expect(resolved.userToken).toBeUndefined();
|
||||
expect(resolved.userTokenSource).toBe("none");
|
||||
expect(resolved.accountId).toBe("default");
|
||||
expect(resolved.config.allowFrom).toEqual(["U999"]);
|
||||
});
|
||||
|
||||
it("does not read socket-only app token for HTTP mode accounts", () => {
|
||||
const resolved = resolveSlackAccount({
|
||||
cfg: {
|
||||
channels: {
|
||||
slack: {
|
||||
accounts: {
|
||||
default: {
|
||||
mode: "http",
|
||||
botToken: "xoxb-resolved",
|
||||
appToken: secretRef,
|
||||
signingSecret: "signing-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig,
|
||||
accountId: "default",
|
||||
tolerateUnresolvedSecrets: true,
|
||||
});
|
||||
|
||||
expect(resolved.botToken).toBe("xoxb-resolved");
|
||||
expect(resolved.botTokenSource).toBe("config");
|
||||
expect(resolved.appToken).toBe("xapp-resolved");
|
||||
expect(resolved.appTokenSource).toBe("config");
|
||||
expect(resolved.appToken).toBeUndefined();
|
||||
expect(resolved.appTokenSource).toBe("none");
|
||||
});
|
||||
|
||||
it("does not silently fall back to SLACK_*_TOKEN env vars in tolerant mode when all credentials are configured as SecretRef (credential confusion guard)", () => {
|
||||
// Each credential is configured as a SecretRef. In tolerant mode none of
|
||||
// them resolves, so per-credential env gating must block all three env
|
||||
// vars; otherwise a stray `SLACK_*_TOKEN` would silently impersonate the
|
||||
// operator-configured account (CWE-287 credential confusion).
|
||||
const cfgAllSecretRefs = {
|
||||
channels: {
|
||||
slack: {
|
||||
accounts: {
|
||||
default: {
|
||||
botToken: { source: "exec", provider: "default", id: "slack_bot_token" },
|
||||
appToken: { source: "exec", provider: "default", id: "slack_app_token" },
|
||||
userToken: { source: "exec", provider: "default", id: "slack_user_token" },
|
||||
it("throws when a socket-mode account still has an unresolved active app token SecretRef", () => {
|
||||
expect(() =>
|
||||
resolveSlackAccount({
|
||||
cfg: {
|
||||
channels: {
|
||||
slack: {
|
||||
accounts: {
|
||||
default: {
|
||||
mode: "socket",
|
||||
botToken: "xoxb-resolved",
|
||||
appToken: secretRef,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
const previousBotToken = process.env.SLACK_BOT_TOKEN;
|
||||
const previousAppToken = process.env.SLACK_APP_TOKEN;
|
||||
const previousUserToken = process.env.SLACK_USER_TOKEN;
|
||||
process.env.SLACK_BOT_TOKEN = "xoxb-env-fallback";
|
||||
process.env.SLACK_APP_TOKEN = "xapp-env-fallback";
|
||||
process.env.SLACK_USER_TOKEN = "xoxp-env-fallback";
|
||||
try {
|
||||
const resolved = resolveSlackAccount({
|
||||
cfg: cfgAllSecretRefs,
|
||||
} as unknown as OpenClawConfig,
|
||||
accountId: "default",
|
||||
tolerateUnresolvedSecrets: true,
|
||||
});
|
||||
|
||||
expect(resolved.botToken).toBeUndefined();
|
||||
expect(resolved.botTokenSource).toBe("none");
|
||||
expect(resolved.appToken).toBeUndefined();
|
||||
expect(resolved.appTokenSource).toBe("none");
|
||||
expect(resolved.userToken).toBeUndefined();
|
||||
expect(resolved.userTokenSource).toBe("none");
|
||||
} finally {
|
||||
if (previousBotToken === undefined) {
|
||||
delete process.env.SLACK_BOT_TOKEN;
|
||||
} else {
|
||||
process.env.SLACK_BOT_TOKEN = previousBotToken;
|
||||
}
|
||||
if (previousAppToken === undefined) {
|
||||
delete process.env.SLACK_APP_TOKEN;
|
||||
} else {
|
||||
process.env.SLACK_APP_TOKEN = previousAppToken;
|
||||
}
|
||||
if (previousUserToken === undefined) {
|
||||
delete process.env.SLACK_USER_TOKEN;
|
||||
} else {
|
||||
process.env.SLACK_USER_TOKEN = previousUserToken;
|
||||
}
|
||||
}
|
||||
}),
|
||||
).toThrowError(/channels\.slack\.accounts\.default\.appToken/);
|
||||
});
|
||||
|
||||
it("preserves SLACK_BOT_TOKEN env fallback in tolerant mode when no config token is set (env-only setups)", () => {
|
||||
it("preserves env fallback when no active config token is set", () => {
|
||||
const previousBotToken = process.env.SLACK_BOT_TOKEN;
|
||||
const previousAppToken = process.env.SLACK_APP_TOKEN;
|
||||
process.env.SLACK_BOT_TOKEN = "xoxb-env-only";
|
||||
@@ -252,7 +230,6 @@ describe("resolveSlackAccount tolerateUnresolvedSecrets", () => {
|
||||
},
|
||||
},
|
||||
accountId: "default",
|
||||
tolerateUnresolvedSecrets: true,
|
||||
});
|
||||
|
||||
expect(resolved.botToken).toBe("xoxb-env-only");
|
||||
@@ -273,36 +250,31 @@ describe("resolveSlackAccount tolerateUnresolvedSecrets", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("blocks env fallback per-credential: unresolved SecretRef on botToken does not leak SLACK_APP_TOKEN", () => {
|
||||
it("does not use env fallback for inactive credentials", () => {
|
||||
const previousBotToken = process.env.SLACK_BOT_TOKEN;
|
||||
const previousAppToken = process.env.SLACK_APP_TOKEN;
|
||||
process.env.SLACK_BOT_TOKEN = "xoxb-env-bot";
|
||||
process.env.SLACK_APP_TOKEN = "xapp-env-app";
|
||||
try {
|
||||
// botToken has an unresolved SecretRef (env fallback should be
|
||||
// blocked), but appToken is unset (env fallback should still fire).
|
||||
// This proves the gating is per-credential, not whole-account.
|
||||
const resolved = resolveSlackAccount({
|
||||
cfg: {
|
||||
channels: {
|
||||
slack: {
|
||||
accounts: {
|
||||
default: {
|
||||
botToken: { source: "exec", provider: "default", id: "slack_bot_token" },
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig,
|
||||
},
|
||||
accountId: "default",
|
||||
tolerateUnresolvedSecrets: true,
|
||||
});
|
||||
|
||||
expect(resolved.botToken).toBeUndefined();
|
||||
expect(resolved.botTokenSource).toBe("none");
|
||||
// appToken was never configured → env fallback still fires.
|
||||
expect(resolved.appToken).toBe("xapp-env-app");
|
||||
expect(resolved.appTokenSource).toBe("env");
|
||||
expect(resolved.appToken).toBeUndefined();
|
||||
expect(resolved.appTokenSource).toBe("none");
|
||||
} finally {
|
||||
if (previousBotToken === undefined) {
|
||||
delete process.env.SLACK_BOT_TOKEN;
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
resolveMergedAccountConfig,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/account-resolution";
|
||||
import { isSecretRef, normalizeSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { SlackAccountSurfaceFields } from "./account-surface-fields.js";
|
||||
import type { SlackAccountConfig } from "./runtime-api.js";
|
||||
@@ -45,28 +44,6 @@ export function mergeSlackAccountConfig(
|
||||
export function resolveSlackAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
/**
|
||||
* When true, account-level credential reads (`botToken`, `appToken`,
|
||||
* `userToken`) silently become `undefined` for unresolved `SecretRef`
|
||||
* inputs instead of throwing. Default is false to preserve the strict
|
||||
* behavior expected by boot-time provider initialization, which must
|
||||
* surface unresolved channel SecretRefs loudly.
|
||||
*
|
||||
* Pass `true` from call sites that already have a separately-resolved
|
||||
* credential override (for example `sendMessageSlack` receives an explicit
|
||||
* `opts.token` derived from the boot-time monitor context) and only need
|
||||
* the rest of the account info (account id, dm policy, channel settings,
|
||||
* etc.). The downstream consumer's existing `if (!token)` guard still
|
||||
* surfaces a clean "missing token" error when no explicit override is
|
||||
* supplied either.
|
||||
*
|
||||
* Without this opt-in, an inactive `channels.slack.accounts.*.botToken`
|
||||
* SecretRef left in the runtime snapshot (per the inspect/strict
|
||||
* separation introduced in #66818) blows up the strict resolver path even
|
||||
* though the actual send already has a valid boot-resolved token. See
|
||||
* #68237.
|
||||
*/
|
||||
tolerateUnresolvedSecrets?: boolean;
|
||||
}): ResolvedSlackAccount {
|
||||
const accountId = normalizeAccountId(
|
||||
params.accountId ?? resolveDefaultSlackAccountId(params.cfg),
|
||||
@@ -75,36 +52,26 @@ export function resolveSlackAccount(params: {
|
||||
const merged = mergeSlackAccountConfig(params.cfg, accountId);
|
||||
const accountEnabled = merged.enabled !== false;
|
||||
const enabled = baseEnabled && accountEnabled;
|
||||
// Per-credential env-var fallback gating: in tolerant mode, only block
|
||||
// the `SLACK_*_TOKEN` env fallback for credentials whose configured value
|
||||
// is an unresolved `SecretRef` object. Otherwise (config field is a
|
||||
// resolved string, or unset entirely) keep the original env fallback so
|
||||
// legitimate env-only setups (no per-account config token, just
|
||||
// `SLACK_BOT_TOKEN` in the process env) keep working. This avoids
|
||||
// credential confusion (CWE-287) on misconfigured deployments where an
|
||||
// unresolved SecretRef would otherwise be silently overridden by a stray
|
||||
// env var, while preserving the env-only contract that callers like
|
||||
// `extensions/slack/src/channel.ts` rely on when omitting `opts.token`.
|
||||
const mode = merged.mode ?? "socket";
|
||||
const baseAllowEnv = accountId === DEFAULT_ACCOUNT_ID;
|
||||
const tolerantMode = params.tolerateUnresolvedSecrets === true;
|
||||
const blockBotEnv = tolerantMode && isSecretRef(merged.botToken);
|
||||
const blockAppEnv = tolerantMode && isSecretRef(merged.appToken);
|
||||
const blockUserEnv = tolerantMode && isSecretRef(merged.userToken);
|
||||
const botActive = enabled;
|
||||
const appActive = enabled && mode !== "http";
|
||||
const userActive = enabled;
|
||||
const envBot =
|
||||
baseAllowEnv && !blockBotEnv ? resolveSlackBotToken(process.env.SLACK_BOT_TOKEN) : undefined;
|
||||
botActive && baseAllowEnv ? resolveSlackBotToken(process.env.SLACK_BOT_TOKEN) : undefined;
|
||||
const envApp =
|
||||
baseAllowEnv && !blockAppEnv ? resolveSlackAppToken(process.env.SLACK_APP_TOKEN) : undefined;
|
||||
appActive && baseAllowEnv ? resolveSlackAppToken(process.env.SLACK_APP_TOKEN) : undefined;
|
||||
const envUser =
|
||||
baseAllowEnv && !blockUserEnv ? resolveSlackUserToken(process.env.SLACK_USER_TOKEN) : undefined;
|
||||
const configBot = tolerantMode
|
||||
? normalizeSecretInputString(merged.botToken)
|
||||
: resolveSlackBotToken(merged.botToken, `channels.slack.accounts.${accountId}.botToken`);
|
||||
const configApp = tolerantMode
|
||||
? normalizeSecretInputString(merged.appToken)
|
||||
: resolveSlackAppToken(merged.appToken, `channels.slack.accounts.${accountId}.appToken`);
|
||||
const configUser = tolerantMode
|
||||
? normalizeSecretInputString(merged.userToken)
|
||||
: resolveSlackUserToken(merged.userToken, `channels.slack.accounts.${accountId}.userToken`);
|
||||
userActive && baseAllowEnv ? resolveSlackUserToken(process.env.SLACK_USER_TOKEN) : undefined;
|
||||
const configBot = botActive
|
||||
? resolveSlackBotToken(merged.botToken, `channels.slack.accounts.${accountId}.botToken`)
|
||||
: undefined;
|
||||
const configApp = appActive
|
||||
? resolveSlackAppToken(merged.appToken, `channels.slack.accounts.${accountId}.appToken`)
|
||||
: undefined;
|
||||
const configUser = userActive
|
||||
? resolveSlackUserToken(merged.userToken, `channels.slack.accounts.${accountId}.userToken`)
|
||||
: undefined;
|
||||
const botToken = configBot ?? envBot;
|
||||
const appToken = configApp ?? envApp;
|
||||
const userToken = configUser ?? envUser;
|
||||
|
||||
@@ -50,11 +50,16 @@ describe("handleSlackAction", () => {
|
||||
}
|
||||
|
||||
function expectLastSlackSend(content: string, threadTs?: string) {
|
||||
expect(sendSlackMessage).toHaveBeenLastCalledWith("channel:C123", content, {
|
||||
mediaUrl: undefined,
|
||||
threadTs,
|
||||
blocks: undefined,
|
||||
});
|
||||
expect(sendSlackMessage).toHaveBeenLastCalledWith(
|
||||
"channel:C123",
|
||||
content,
|
||||
expect.objectContaining({
|
||||
cfg: expect.any(Object),
|
||||
mediaUrl: undefined,
|
||||
threadTs,
|
||||
blocks: undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function sendSecondMessageAndExpectNoThread(params: {
|
||||
@@ -119,7 +124,12 @@ describe("handleSlackAction", () => {
|
||||
},
|
||||
slackConfig(),
|
||||
);
|
||||
expect(reactSlackMessage).toHaveBeenCalledWith("C1", "123.456", "✅");
|
||||
expect(reactSlackMessage).toHaveBeenCalledWith(
|
||||
"C1",
|
||||
"123.456",
|
||||
"✅",
|
||||
expect.objectContaining({ cfg: expect.any(Object) }),
|
||||
);
|
||||
});
|
||||
|
||||
it("removes reactions on empty emoji", async () => {
|
||||
@@ -132,7 +142,11 @@ describe("handleSlackAction", () => {
|
||||
},
|
||||
slackConfig(),
|
||||
);
|
||||
expect(removeOwnSlackReactions).toHaveBeenCalledWith("C1", "123.456");
|
||||
expect(removeOwnSlackReactions).toHaveBeenCalledWith(
|
||||
"C1",
|
||||
"123.456",
|
||||
expect.objectContaining({ cfg: expect.any(Object) }),
|
||||
);
|
||||
});
|
||||
|
||||
it("removes reactions when remove flag set", async () => {
|
||||
@@ -146,7 +160,12 @@ describe("handleSlackAction", () => {
|
||||
},
|
||||
slackConfig(),
|
||||
);
|
||||
expect(removeSlackReaction).toHaveBeenCalledWith("C1", "123.456", "✅");
|
||||
expect(removeSlackReaction).toHaveBeenCalledWith(
|
||||
"C1",
|
||||
"123.456",
|
||||
"✅",
|
||||
expect.objectContaining({ cfg: expect.any(Object) }),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects removes without emoji", async () => {
|
||||
@@ -188,11 +207,16 @@ describe("handleSlackAction", () => {
|
||||
},
|
||||
slackConfig(),
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "Hello thread", {
|
||||
mediaUrl: undefined,
|
||||
threadTs: "1234567890.123456",
|
||||
blocks: undefined,
|
||||
});
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith(
|
||||
"channel:C123",
|
||||
"Hello thread",
|
||||
expect.objectContaining({
|
||||
cfg: expect.any(Object),
|
||||
mediaUrl: undefined,
|
||||
threadTs: "1234567890.123456",
|
||||
blocks: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns a friendly error when downloadFile cannot fetch the attachment", async () => {
|
||||
@@ -289,11 +313,16 @@ describe("handleSlackAction", () => {
|
||||
},
|
||||
slackConfig(),
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "", {
|
||||
mediaUrl: undefined,
|
||||
threadTs: undefined,
|
||||
blocks: expectedBlocks,
|
||||
});
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith(
|
||||
"channel:C123",
|
||||
"",
|
||||
expect.objectContaining({
|
||||
cfg: expect.any(Object),
|
||||
mediaUrl: undefined,
|
||||
threadTs: undefined,
|
||||
blocks: expectedBlocks,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -344,12 +373,17 @@ describe("handleSlackAction", () => {
|
||||
slackConfig(),
|
||||
);
|
||||
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith("user:U123", "fresh report", {
|
||||
mediaUrl: "/tmp/report.png",
|
||||
threadTs: "111.222",
|
||||
uploadFileName: "report-final.png",
|
||||
uploadTitle: "Report Final",
|
||||
});
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith(
|
||||
"user:U123",
|
||||
"fresh report",
|
||||
expect.objectContaining({
|
||||
cfg: expect.any(Object),
|
||||
mediaUrl: "/tmp/report.png",
|
||||
threadTs: "111.222",
|
||||
uploadFileName: "report-final.png",
|
||||
uploadTitle: "Report Final",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects blocks combined with mediaUrl", async () => {
|
||||
@@ -389,9 +423,15 @@ describe("handleSlackAction", () => {
|
||||
},
|
||||
slackConfig(),
|
||||
);
|
||||
expect(editSlackMessage).toHaveBeenCalledWith("C123", "123.456", "", {
|
||||
blocks: expectedBlocks,
|
||||
});
|
||||
expect(editSlackMessage).toHaveBeenCalledWith(
|
||||
"C123",
|
||||
"123.456",
|
||||
"",
|
||||
expect.objectContaining({
|
||||
cfg: expect.any(Object),
|
||||
blocks: expectedBlocks,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("requires content or blocks for editMessage", async () => {
|
||||
@@ -493,11 +533,16 @@ describe("handleSlackAction", () => {
|
||||
replyToMode: "all",
|
||||
},
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith("channel:C999", "Other channel", {
|
||||
mediaUrl: undefined,
|
||||
threadTs: undefined,
|
||||
blocks: undefined,
|
||||
});
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith(
|
||||
"channel:C999",
|
||||
"Other channel",
|
||||
expect.objectContaining({
|
||||
cfg: expect.any(Object),
|
||||
mediaUrl: undefined,
|
||||
threadTs: undefined,
|
||||
blocks: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("explicit threadTs overrides context threadTs", async () => {
|
||||
@@ -528,11 +573,16 @@ describe("handleSlackAction", () => {
|
||||
replyToMode: "all",
|
||||
},
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith("C123", "Bare target", {
|
||||
mediaUrl: undefined,
|
||||
threadTs: "1111111111.111111",
|
||||
blocks: undefined,
|
||||
});
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith(
|
||||
"C123",
|
||||
"Bare target",
|
||||
expect.objectContaining({
|
||||
cfg: expect.any(Object),
|
||||
mediaUrl: undefined,
|
||||
threadTs: "1111111111.111111",
|
||||
blocks: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("adds normalized timestamps to readMessages payloads", async () => {
|
||||
@@ -568,12 +618,16 @@ describe("handleSlackAction", () => {
|
||||
slackConfig(),
|
||||
);
|
||||
|
||||
expect(readSlackMessages).toHaveBeenCalledWith("C1", {
|
||||
threadId: "1712345678.123456",
|
||||
limit: undefined,
|
||||
before: undefined,
|
||||
after: undefined,
|
||||
});
|
||||
expect(readSlackMessages).toHaveBeenCalledWith(
|
||||
"C1",
|
||||
expect.objectContaining({
|
||||
cfg: expect.any(Object),
|
||||
threadId: "1712345678.123456",
|
||||
limit: undefined,
|
||||
before: undefined,
|
||||
after: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("adds normalized timestamps to pin payloads", async () => {
|
||||
|
||||
@@ -172,10 +172,8 @@ export async function handleSlackAction(
|
||||
const buildActionOpts = (operation: "read" | "write") => {
|
||||
const token = getTokenForOperation(operation);
|
||||
const tokenOverride = token && token !== botToken ? token : undefined;
|
||||
if (!accountId && !tokenOverride) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
cfg,
|
||||
...(accountId ? { accountId } : {}),
|
||||
...(tokenOverride ? { token: tokenOverride } : {}),
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Block, KnownBlock, WebClient } from "@slack/web-api";
|
||||
import { loadConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { requireRuntimeConfig, type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { resolveSlackAccount } from "./accounts.js";
|
||||
import { buildSlackBlocksFallbackText } from "./blocks-fallback.js";
|
||||
@@ -11,6 +11,7 @@ import { sendMessageSlack } from "./send.js";
|
||||
import { resolveSlackBotToken } from "./token.js";
|
||||
|
||||
export type SlackActionClientOpts = {
|
||||
cfg?: OpenClawConfig;
|
||||
accountId?: string;
|
||||
token?: string;
|
||||
client?: WebClient;
|
||||
@@ -41,10 +42,21 @@ export type SlackPin = {
|
||||
file?: { id?: string; name?: string };
|
||||
};
|
||||
|
||||
function resolveToken(explicit?: string, accountId?: string) {
|
||||
const cfg = loadConfig();
|
||||
const account = resolveSlackAccount({ cfg, accountId });
|
||||
const token = resolveSlackBotToken(explicit ?? account.botToken ?? undefined);
|
||||
function resolveToken(explicit?: string, accountId?: string, cfg?: OpenClawConfig): string {
|
||||
if (explicit?.trim()) {
|
||||
const token = resolveSlackBotToken(explicit);
|
||||
if (token) {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
if (!cfg) {
|
||||
throw new Error(
|
||||
"Slack actions requires a resolved runtime config. Load and resolve config at the command or gateway boundary, then pass cfg through the runtime path.",
|
||||
);
|
||||
}
|
||||
const resolvedCfg = requireRuntimeConfig(cfg, "Slack actions");
|
||||
const account = resolveSlackAccount({ cfg: resolvedCfg, accountId });
|
||||
const token = resolveSlackBotToken(account.botToken ?? undefined);
|
||||
if (!token) {
|
||||
logVerbose(
|
||||
`slack actions: missing bot token for account=${account.accountId} explicit=${Boolean(
|
||||
@@ -68,7 +80,7 @@ async function getClient(opts: SlackActionClientOpts = {}, mode: "read" | "write
|
||||
if (opts.client) {
|
||||
return opts.client;
|
||||
}
|
||||
const token = resolveToken(opts.token, opts.accountId);
|
||||
const token = resolveToken(opts.token, opts.accountId, opts.cfg);
|
||||
return mode === "write" ? createSlackWriteClient(token) : createSlackWebClient(token);
|
||||
}
|
||||
|
||||
@@ -160,7 +172,8 @@ export async function listSlackReactions(
|
||||
export async function sendSlackMessage(
|
||||
to: string,
|
||||
content: string,
|
||||
opts: SlackActionClientOpts & {
|
||||
opts: Omit<SlackActionClientOpts, "cfg"> & {
|
||||
cfg: OpenClawConfig;
|
||||
mediaUrl?: string;
|
||||
mediaAccess?: {
|
||||
localRoots?: readonly string[];
|
||||
@@ -172,10 +185,11 @@ export async function sendSlackMessage(
|
||||
uploadFileName?: string;
|
||||
uploadTitle?: string;
|
||||
blocks?: (Block | KnownBlock)[];
|
||||
} = {},
|
||||
},
|
||||
) {
|
||||
return await sendMessageSlack(to, content, {
|
||||
accountId: opts.accountId,
|
||||
cfg: opts.cfg,
|
||||
token: opts.token,
|
||||
mediaUrl: opts.mediaUrl,
|
||||
mediaAccess: opts.mediaAccess,
|
||||
|
||||
@@ -216,6 +216,11 @@ describe("slackPlugin actions", () => {
|
||||
expect(sendMessageSlackMock).toHaveBeenCalledWith(
|
||||
"user:U12345678",
|
||||
expect.stringContaining("approved"),
|
||||
expect.objectContaining({
|
||||
accountId: "work",
|
||||
cfg,
|
||||
token: "xoxb-work",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ import { SLACK_TEXT_LIMIT } from "./limits.js";
|
||||
import { slackOutbound } from "./outbound-adapter.js";
|
||||
import type { SlackProbe } from "./probe.js";
|
||||
import { resolveSlackReplyBlocks } from "./reply-blocks.js";
|
||||
import { getOptionalSlackRuntime, getSlackRuntime } from "./runtime.js";
|
||||
import { getOptionalSlackRuntime } from "./runtime.js";
|
||||
import { fetchSlackScopes } from "./scopes.js";
|
||||
import { slackSecurityAdapter } from "./security.js";
|
||||
import { slackSetupAdapter } from "./setup-core.js";
|
||||
@@ -153,9 +153,8 @@ async function resolveSlackSendContext(params: {
|
||||
resolveOutboundSendDep<SlackSendFn>(params.deps, "slack") ??
|
||||
(await loadSlackSendRuntime()).sendMessageSlack;
|
||||
// params.cfg is the scoped channel-dispatch config; channel credentials are
|
||||
// expected to be resolved here (not a raw loadConfig() snapshot). Strict mode
|
||||
// is intentional so boot-time misconfigurations surface loudly. See #68237
|
||||
// for the companion tolerant-mode path in sendMessageSlack itself.
|
||||
// expected to be resolved from this snapshot. Strict mode
|
||||
// is intentional so boot-time misconfigurations surface loudly. See #68237.
|
||||
const account = resolveSlackAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const token = getTokenForOperation(account, "write");
|
||||
const botToken = account.botToken?.trim();
|
||||
@@ -513,23 +512,18 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount, SlackProbe> = crea
|
||||
idLabel: "slackUserId",
|
||||
message: PAIRING_APPROVED_MESSAGE,
|
||||
normalizeAllowEntry: createPairingPrefixStripper(/^(slack|user):/i),
|
||||
notify: async ({ id, message }) => {
|
||||
const cfg = getSlackRuntime().config.loadConfig();
|
||||
notify: async ({ cfg, id, message }) => {
|
||||
const account = resolveSlackAccount({
|
||||
cfg,
|
||||
accountId: resolveDefaultSlackAccountId(cfg),
|
||||
});
|
||||
const { sendMessageSlack } = await loadSlackSendRuntime();
|
||||
const token = getTokenForOperation(account, "write");
|
||||
const botToken = account.botToken?.trim();
|
||||
const tokenOverride = token && token !== botToken ? token : undefined;
|
||||
if (tokenOverride) {
|
||||
await sendMessageSlack(`user:${id}`, message, {
|
||||
token: tokenOverride,
|
||||
});
|
||||
} else {
|
||||
await sendMessageSlack(`user:${id}`, message);
|
||||
}
|
||||
await sendMessageSlack(`user:${id}`, message, {
|
||||
cfg,
|
||||
accountId: account.accountId,
|
||||
...(token ? { token } : {}),
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,6 +7,8 @@ type DraftEditFn = NonNullable<DraftStreamParams["edit"]>;
|
||||
type DraftRemoveFn = NonNullable<DraftStreamParams["remove"]>;
|
||||
type DraftWarnFn = NonNullable<DraftStreamParams["warn"]>;
|
||||
|
||||
const TEST_CFG = {};
|
||||
|
||||
function createDraftStreamHarness(
|
||||
params: {
|
||||
maxChars?: number;
|
||||
@@ -27,6 +29,7 @@ function createDraftStreamHarness(
|
||||
const warn = params.warn ?? vi.fn<DraftWarnFn>();
|
||||
const stream = createSlackDraftStream({
|
||||
target: "channel:C123",
|
||||
cfg: TEST_CFG,
|
||||
token: "xoxb-test",
|
||||
throttleMs: 250,
|
||||
maxChars: params.maxChars,
|
||||
@@ -50,6 +53,7 @@ describe("createSlackDraftStream", () => {
|
||||
expect(send).toHaveBeenCalledTimes(1);
|
||||
expect(edit).toHaveBeenCalledTimes(1);
|
||||
expect(edit).toHaveBeenCalledWith("C123", "111.222", "hello world", {
|
||||
cfg: TEST_CFG,
|
||||
token: "xoxb-test",
|
||||
accountId: undefined,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createDraftStreamLoop } from "openclaw/plugin-sdk/channel-lifecycle";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { deleteSlackMessage, editSlackMessage } from "./actions.js";
|
||||
import { SLACK_TEXT_LIMIT } from "./limits.js";
|
||||
@@ -20,6 +21,7 @@ export type SlackDraftStream = {
|
||||
|
||||
export function createSlackDraftStream(params: {
|
||||
target: string;
|
||||
cfg: OpenClawConfig;
|
||||
token: string;
|
||||
accountId?: string;
|
||||
maxChars?: number;
|
||||
@@ -63,12 +65,14 @@ export function createSlackDraftStream(params: {
|
||||
try {
|
||||
if (streamChannelId && streamMessageId) {
|
||||
await edit(streamChannelId, streamMessageId, trimmed, {
|
||||
cfg: params.cfg,
|
||||
token: params.token,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const sent = await send(params.target, trimmed, {
|
||||
cfg: params.cfg,
|
||||
token: params.token,
|
||||
accountId: params.accountId,
|
||||
threadTs: params.resolveThreadTs?.(),
|
||||
|
||||
@@ -443,6 +443,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
return;
|
||||
}
|
||||
await deliverReplies({
|
||||
cfg: ctx.cfg,
|
||||
replies: [params.payload],
|
||||
target: prepared.replyTarget,
|
||||
token: ctx.botToken,
|
||||
@@ -677,6 +678,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
const draftStream = shouldUseDraftStream
|
||||
? createSlackDraftStream({
|
||||
target: prepared.replyTarget,
|
||||
cfg,
|
||||
token: ctx.botToken,
|
||||
accountId: account.accountId,
|
||||
maxChars: Math.min(ctx.textLimit, SLACK_TEXT_LIMIT),
|
||||
|
||||
@@ -235,6 +235,7 @@ async function authorizeSlackInboundMessage(params: {
|
||||
resolveSenderName: ctx.resolveUserName,
|
||||
sendPairingReply: async (text) => {
|
||||
await sendMessageSlack(message.channel, text, {
|
||||
cfg: ctx.cfg,
|
||||
token: ctx.botToken,
|
||||
client: ctx.app.client,
|
||||
accountId: account.accountId,
|
||||
|
||||
@@ -10,8 +10,11 @@ let createSlackReplyDeliveryPlan: typeof import("./replies.js").createSlackReply
|
||||
let resolveSlackThreadTs: typeof import("./replies.js").resolveSlackThreadTs;
|
||||
import { deliverSlackSlashReplies } from "./replies.js";
|
||||
|
||||
const SLACK_TEST_CFG = { channels: { slack: { botToken: "xoxb-test" } } };
|
||||
|
||||
function baseParams(overrides?: Record<string, unknown>) {
|
||||
return {
|
||||
cfg: SLACK_TEST_CFG,
|
||||
replies: [{ text: "hello" }],
|
||||
target: "C123",
|
||||
token: "xoxb-test",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MarkdownTableMode } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { MarkdownTableMode, OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
deliverTextOrMediaReply,
|
||||
resolveSendableOutboundReplyParts,
|
||||
@@ -22,6 +22,7 @@ export function readSlackReplyBlocks(payload: ReplyPayload) {
|
||||
}
|
||||
|
||||
export async function deliverReplies(params: {
|
||||
cfg: OpenClawConfig;
|
||||
replies: ReplyPayload[];
|
||||
target: string;
|
||||
token: string;
|
||||
@@ -52,6 +53,7 @@ export async function deliverReplies(params: {
|
||||
continue;
|
||||
}
|
||||
await sendMessageSlack(params.target, trimmed, {
|
||||
cfg: params.cfg,
|
||||
token: params.token,
|
||||
threadTs,
|
||||
accountId: params.accountId,
|
||||
@@ -76,6 +78,7 @@ export async function deliverReplies(params: {
|
||||
: undefined,
|
||||
sendText: async (trimmed) => {
|
||||
await sendMessageSlack(params.target, trimmed, {
|
||||
cfg: params.cfg,
|
||||
token: params.token,
|
||||
threadTs,
|
||||
accountId: params.accountId,
|
||||
@@ -84,6 +87,7 @@ export async function deliverReplies(params: {
|
||||
},
|
||||
sendMedia: async ({ mediaUrl, caption }) => {
|
||||
await sendMessageSlack(params.target, caption ?? "", {
|
||||
cfg: params.cfg,
|
||||
token: params.token,
|
||||
mediaUrl,
|
||||
threadTs,
|
||||
|
||||
@@ -3,12 +3,14 @@ import { createSlackSendTestClient, installSlackBlockTestMocks } from "./blocks.
|
||||
|
||||
installSlackBlockTestMocks();
|
||||
const { sendMessageSlack } = await import("./send.js");
|
||||
const SLACK_TEST_CFG = { channels: { slack: { botToken: "xoxb-test" } } };
|
||||
|
||||
describe("sendMessageSlack NO_REPLY guard", () => {
|
||||
it("suppresses NO_REPLY text before any Slack API call", async () => {
|
||||
const client = createSlackSendTestClient();
|
||||
const result = await sendMessageSlack("channel:C123", "NO_REPLY", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
});
|
||||
|
||||
@@ -20,6 +22,7 @@ describe("sendMessageSlack NO_REPLY guard", () => {
|
||||
const client = createSlackSendTestClient();
|
||||
const result = await sendMessageSlack("channel:C123", " NO_REPLY ", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
});
|
||||
|
||||
@@ -31,6 +34,7 @@ describe("sendMessageSlack NO_REPLY guard", () => {
|
||||
const client = createSlackSendTestClient();
|
||||
await sendMessageSlack("channel:C123", "This is not a NO_REPLY situation", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
});
|
||||
|
||||
@@ -41,6 +45,7 @@ describe("sendMessageSlack NO_REPLY guard", () => {
|
||||
const client = createSlackSendTestClient();
|
||||
const result = await sendMessageSlack("channel:C123", "NO_REPLY", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
blocks: [{ type: "section", text: { type: "mrkdwn", text: "content" } }],
|
||||
});
|
||||
@@ -57,6 +62,7 @@ describe("sendMessageSlack chunking", () => {
|
||||
|
||||
await sendMessageSlack("channel:C123", message, {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
});
|
||||
|
||||
@@ -75,6 +81,7 @@ describe("sendMessageSlack blocks", () => {
|
||||
const client = createSlackSendTestClient();
|
||||
const result = await sendMessageSlack("channel:C123", "", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
blocks: [{ type: "divider" }],
|
||||
});
|
||||
@@ -94,6 +101,7 @@ describe("sendMessageSlack blocks", () => {
|
||||
const client = createSlackSendTestClient();
|
||||
await sendMessageSlack("channel:C123", "", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
blocks: [{ type: "image", image_url: "https://example.com/a.png", alt_text: "Build chart" }],
|
||||
});
|
||||
@@ -109,6 +117,7 @@ describe("sendMessageSlack blocks", () => {
|
||||
const client = createSlackSendTestClient();
|
||||
await sendMessageSlack("channel:C123", "", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
blocks: [
|
||||
{
|
||||
@@ -132,6 +141,7 @@ describe("sendMessageSlack blocks", () => {
|
||||
const client = createSlackSendTestClient();
|
||||
await sendMessageSlack("channel:C123", "", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
blocks: [{ type: "file", source: "remote", external_id: "F123" }],
|
||||
});
|
||||
@@ -148,6 +158,7 @@ describe("sendMessageSlack blocks", () => {
|
||||
await expect(
|
||||
sendMessageSlack("channel:C123", "hi", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
mediaUrl: "https://example.com/image.png",
|
||||
blocks: [{ type: "divider" }],
|
||||
@@ -161,6 +172,7 @@ describe("sendMessageSlack blocks", () => {
|
||||
await expect(
|
||||
sendMessageSlack("channel:C123", "hi", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
blocks: [],
|
||||
}),
|
||||
@@ -174,6 +186,7 @@ describe("sendMessageSlack blocks", () => {
|
||||
await expect(
|
||||
sendMessageSlack("channel:C123", "hi", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
blocks,
|
||||
}),
|
||||
@@ -186,6 +199,7 @@ describe("sendMessageSlack blocks", () => {
|
||||
await expect(
|
||||
sendMessageSlack("channel:C123", "hi", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
blocks: [{} as { type: string }],
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type Block, type KnownBlock, type WebClient } from "@slack/web-api";
|
||||
import { loadConfig, type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { requireRuntimeConfig, type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { withTrustedEnvProxyGuardedFetchMode } from "openclaw/plugin-sdk/fetch-runtime";
|
||||
import {
|
||||
@@ -49,7 +49,7 @@ export type SlackSendIdentity = {
|
||||
};
|
||||
|
||||
type SlackSendOpts = {
|
||||
cfg?: OpenClawConfig;
|
||||
cfg: OpenClawConfig;
|
||||
token?: string;
|
||||
accountId?: string;
|
||||
mediaUrl?: string;
|
||||
@@ -310,7 +310,7 @@ async function uploadSlackFile(params: {
|
||||
export async function sendMessageSlack(
|
||||
to: string,
|
||||
message: string,
|
||||
opts: SlackSendOpts = {},
|
||||
opts: SlackSendOpts,
|
||||
): Promise<SlackSendResult> {
|
||||
const trimmedMessage = normalizeOptionalString(message) ?? "";
|
||||
if (isSilentReplyText(trimmedMessage) && !opts.mediaUrl && !opts.blocks) {
|
||||
@@ -321,21 +321,10 @@ export async function sendMessageSlack(
|
||||
if (!trimmedMessage && !opts.mediaUrl && !blocks) {
|
||||
throw new Error("Slack send requires text, blocks, or media");
|
||||
}
|
||||
const cfg = opts.cfg ?? loadConfig();
|
||||
// Tolerate unresolved channel SecretRefs in the cfg snapshot here: the
|
||||
// send path either receives an explicit `opts.token` (resolved at Slack
|
||||
// monitor boot time and threaded through `ctx.botToken`) or surfaces the
|
||||
// existing "Slack bot token missing" error via `resolveToken` below. The
|
||||
// runtime snapshot can legitimately retain unresolved `channels.slack.*`
|
||||
// SecretRefs (see the inspect/strict separation introduced in #66818) when
|
||||
// the active account's secrets were not part of the agent-runtime base
|
||||
// target set; failing the strict resolver here would block outbound
|
||||
// replies even though `reactions.add` and inbound dispatch (which use the
|
||||
// boot-resolved client/token directly) keep working. See #68237.
|
||||
const cfg = requireRuntimeConfig(opts.cfg, "Slack send");
|
||||
const account = resolveSlackAccount({
|
||||
cfg,
|
||||
accountId: opts.accountId,
|
||||
tolerateUnresolvedSecrets: true,
|
||||
});
|
||||
const token = resolveToken({
|
||||
explicit: opts.token,
|
||||
|
||||
@@ -44,6 +44,7 @@ vi.mock("./runtime-api.js", async () => {
|
||||
let sendMessageSlack: typeof import("./send.js").sendMessageSlack;
|
||||
let clearSlackDmChannelCache: typeof import("./send.js").clearSlackDmChannelCache;
|
||||
({ sendMessageSlack, clearSlackDmChannelCache } = await import("./send.js"));
|
||||
const SLACK_TEST_CFG = { channels: { slack: { botToken: "xoxb-test" } } };
|
||||
|
||||
type UploadTestClient = WebClient & {
|
||||
conversations: { open: ReturnType<typeof vi.fn> };
|
||||
@@ -96,6 +97,7 @@ describe("sendMessageSlack file upload with user IDs", () => {
|
||||
// Bare user ID — parseSlackTarget classifies this as kind="channel"
|
||||
await sendMessageSlack("U2ZH3MFSR", "screenshot", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
mediaUrl: "/tmp/screenshot.png",
|
||||
});
|
||||
@@ -118,6 +120,7 @@ describe("sendMessageSlack file upload with user IDs", () => {
|
||||
|
||||
await sendMessageSlack("user:UABC123", "image", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
mediaUrl: "/tmp/photo.png",
|
||||
});
|
||||
@@ -135,10 +138,12 @@ describe("sendMessageSlack file upload with user IDs", () => {
|
||||
|
||||
await sendMessageSlack("user:UABC123", "first", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
});
|
||||
await sendMessageSlack("user:UABC123", "second", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
});
|
||||
|
||||
@@ -158,10 +163,12 @@ describe("sendMessageSlack file upload with user IDs", () => {
|
||||
|
||||
await sendMessageSlack("user:UABC123", "first", {
|
||||
token: "xoxb-test-a",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
});
|
||||
await sendMessageSlack("user:UABC123", "second", {
|
||||
token: "xoxb-test-b",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
});
|
||||
|
||||
@@ -173,6 +180,7 @@ describe("sendMessageSlack file upload with user IDs", () => {
|
||||
|
||||
await sendMessageSlack("channel:C123CHAN", "chart", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
mediaUrl: "/tmp/chart.png",
|
||||
});
|
||||
@@ -188,6 +196,7 @@ describe("sendMessageSlack file upload with user IDs", () => {
|
||||
|
||||
await sendMessageSlack("<@U777TEST>", "report", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
mediaUrl: "/tmp/report.png",
|
||||
});
|
||||
@@ -205,6 +214,7 @@ describe("sendMessageSlack file upload with user IDs", () => {
|
||||
|
||||
await sendMessageSlack("channel:C123CHAN", "caption", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
mediaUrl: "/tmp/threaded.png",
|
||||
threadTs: "171.222",
|
||||
@@ -241,6 +251,7 @@ describe("sendMessageSlack file upload with user IDs", () => {
|
||||
|
||||
await sendMessageSlack("channel:C123CHAN", "caption", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
mediaUrl: "/tmp/threaded.png",
|
||||
uploadFileName: "custom-name.bin",
|
||||
@@ -263,6 +274,7 @@ describe("sendMessageSlack file upload with user IDs", () => {
|
||||
|
||||
await sendMessageSlack("channel:C123CHAN", "caption", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
mediaUrl: "/tmp/threaded.png",
|
||||
uploadFileName: "custom-name.bin",
|
||||
|
||||
Reference in New Issue
Block a user