openclaw/extensions/slack/src/approval-native.test.ts

337 lines
9.2 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { describe, expect, it } from "vitest";
import { clearSessionStoreCacheForTest } from "../../../src/config/sessions.js";
import { slackNativeApprovalAdapter } from "./approval-native.js";
function buildConfig(
overrides?: Partial<NonNullable<NonNullable<OpenClawConfig["channels"]>["slack"]>>,
): OpenClawConfig {
return {
channels: {
slack: {
botToken: "xoxb-test",
appToken: "xapp-test",
execApprovals: {
enabled: true,
approvers: ["U123APPROVER"],
target: "both",
},
...overrides,
},
},
} as OpenClawConfig;
}
const STORE_PATH = path.join(os.tmpdir(), "openclaw-slack-approval-native-test.json");
function writeStore(store: Record<string, unknown>) {
fs.writeFileSync(STORE_PATH, `${JSON.stringify(store, null, 2)}\n`, "utf8");
clearSessionStoreCacheForTest();
}
describe("slack native approval adapter", () => {
it("describes native slack approval delivery capabilities", () => {
const capabilities = slackNativeApprovalAdapter.native?.describeDeliveryCapabilities({
cfg: buildConfig(),
accountId: "default",
approvalKind: "exec",
request: {
id: "req-1",
request: {
command: "echo hi",
turnSourceChannel: "slack",
turnSourceTo: "channel:C123",
turnSourceAccountId: "default",
sessionKey: "agent:main:slack:channel:c123",
},
createdAtMs: 0,
expiresAtMs: 1000,
},
});
expect(capabilities).toEqual({
enabled: true,
preferredSurface: "both",
supportsOriginSurface: true,
supportsApproverDmSurface: true,
notifyOriginWhenDmOnly: true,
});
});
it("resolves origin targets from slack turn source", async () => {
const target = await slackNativeApprovalAdapter.native?.resolveOriginTarget?.({
cfg: buildConfig(),
accountId: "default",
approvalKind: "exec",
request: {
id: "req-1",
request: {
command: "echo hi",
turnSourceChannel: "slack",
turnSourceTo: "channel:C123",
turnSourceAccountId: "default",
turnSourceThreadId: "1712345678.123456",
sessionKey: "agent:main:slack:channel:c123:thread:1712345678.123456",
},
createdAtMs: 0,
expiresAtMs: 1000,
},
});
expect(target).toEqual({
to: "channel:C123",
threadId: "1712345678.123456",
});
});
it("keeps origin delivery when session and turn source thread ids differ only by Slack timestamp precision", async () => {
const target = await slackNativeApprovalAdapter.native?.resolveOriginTarget?.({
cfg: buildConfig(),
accountId: "default",
approvalKind: "exec",
request: {
id: "req-1",
request: {
command: "echo hi",
turnSourceChannel: "slack",
turnSourceTo: "channel:C123",
turnSourceAccountId: "default",
turnSourceThreadId: "1712345678.123456",
sessionKey: "agent:main:slack:channel:c123:thread:1712345678.123456",
},
createdAtMs: 0,
expiresAtMs: 1000,
},
});
expect(target).toEqual({
to: "channel:C123",
threadId: "1712345678.123456",
});
});
it("resolves approver dm targets", async () => {
const targets = await slackNativeApprovalAdapter.native?.resolveApproverDmTargets?.({
cfg: buildConfig(),
accountId: "default",
approvalKind: "exec",
request: {
id: "req-1",
request: {
command: "echo hi",
},
createdAtMs: 0,
expiresAtMs: 1000,
},
});
expect(targets).toEqual([{ to: "user:U123APPROVER" }]);
});
it("falls back to the session-bound origin target for plugin approvals", async () => {
writeStore({
"agent:main:slack:channel:c123": {
sessionId: "sess",
updatedAt: Date.now(),
deliveryContext: {
channel: "slack",
to: "channel:C123",
accountId: "default",
threadId: "1712345678.123456",
},
},
});
const target = await slackNativeApprovalAdapter.native?.resolveOriginTarget?.({
cfg: {
...buildConfig(),
session: { store: STORE_PATH },
},
accountId: "default",
approvalKind: "plugin",
request: {
id: "plugin:req-1",
request: {
title: "Plugin approval",
description: "Allow access",
sessionKey: "agent:main:slack:channel:c123",
},
createdAtMs: 0,
expiresAtMs: 1000,
},
});
expect(target).toEqual({
to: "channel:C123",
threadId: "1712345678",
});
});
it("skips native delivery when agent filters do not match", async () => {
const cfg = buildConfig({
execApprovals: {
enabled: true,
approvers: ["U123APPROVER"],
target: "both",
agentFilter: ["ops-agent"],
},
});
const originTarget = await slackNativeApprovalAdapter.native?.resolveOriginTarget?.({
cfg,
accountId: "default",
approvalKind: "exec",
request: {
id: "req-1",
request: {
command: "echo hi",
agentId: "other-agent",
turnSourceChannel: "slack",
turnSourceTo: "channel:C123",
turnSourceAccountId: "default",
sessionKey: "agent:other-agent:slack:channel:c123",
},
createdAtMs: 0,
expiresAtMs: 1000,
},
});
const dmTargets = await slackNativeApprovalAdapter.native?.resolveApproverDmTargets?.({
cfg,
accountId: "default",
approvalKind: "exec",
request: {
id: "req-1",
request: {
command: "echo hi",
agentId: "other-agent",
sessionKey: "agent:other-agent:slack:channel:c123",
},
createdAtMs: 0,
expiresAtMs: 1000,
},
});
expect(originTarget).toBeNull();
expect(dmTargets).toEqual([]);
});
it("skips native delivery when the request is bound to another Slack account", async () => {
const originTarget = await slackNativeApprovalAdapter.native?.resolveOriginTarget?.({
cfg: buildConfig(),
accountId: "default",
approvalKind: "exec",
request: {
id: "req-1",
request: {
command: "echo hi",
turnSourceChannel: "slack",
turnSourceTo: "channel:C123",
turnSourceAccountId: "other",
sessionKey: "agent:main:missing",
},
createdAtMs: 0,
expiresAtMs: 1000,
},
});
const dmTargets = await slackNativeApprovalAdapter.native?.resolveApproverDmTargets?.({
cfg: buildConfig(),
accountId: "default",
approvalKind: "exec",
request: {
id: "req-1",
request: {
command: "echo hi",
turnSourceChannel: "slack",
turnSourceAccountId: "other",
sessionKey: "agent:main:missing",
},
createdAtMs: 0,
expiresAtMs: 1000,
},
});
expect(originTarget).toBeNull();
expect(dmTargets).toEqual([]);
});
it("suppresses generic slack fallback only for slack-originated approvals", () => {
const shouldSuppress = slackNativeApprovalAdapter.delivery.shouldSuppressForwardingFallback;
if (!shouldSuppress) {
throw new Error("slack native delivery suppression unavailable");
}
expect(
shouldSuppress({
cfg: buildConfig(),
target: { channel: "slack", accountId: "default" },
request: {
request: {
turnSourceChannel: "slack",
turnSourceAccountId: "default",
},
},
}),
).toBe(true);
expect(
shouldSuppress({
cfg: buildConfig(),
target: { channel: "slack", accountId: "default" },
request: {
request: {
turnSourceChannel: "discord",
turnSourceAccountId: "default",
},
},
}),
).toBe(false);
});
it("keeps plugin approval auth independent from exec approvers", () => {
const cfg = buildConfig({
allowFrom: ["U123OWNER"],
execApprovals: {
enabled: true,
approvers: ["U999EXEC"],
target: "both",
},
});
expect(
slackNativeApprovalAdapter.auth.authorizeActorAction({
cfg,
accountId: "default",
senderId: "U123OWNER",
action: "approve",
approvalKind: "plugin",
}),
).toEqual({ authorized: true });
expect(
slackNativeApprovalAdapter.auth.authorizeActorAction({
cfg,
accountId: "default",
senderId: "U999EXEC",
action: "approve",
approvalKind: "plugin",
}),
).toEqual({
authorized: false,
reason: "❌ You are not authorized to approve plugin requests on Slack.",
});
expect(
slackNativeApprovalAdapter.auth.authorizeActorAction({
cfg,
accountId: "default",
senderId: "U999EXEC",
action: "approve",
approvalKind: "exec",
}),
).toEqual({ authorized: true });
});
});