mirror of https://github.com/openclaw/openclaw.git
337 lines
9.2 KiB
TypeScript
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 });
|
|
});
|
|
});
|