mirror of https://github.com/openclaw/openclaw.git
311 lines
8.7 KiB
TypeScript
311 lines
8.7 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import type { OpenClawConfig } from "../../../src/config/config.js";
|
|
import { updateSessionStore } from "../../../src/config/sessions.js";
|
|
import { TelegramExecApprovalHandler } from "./exec-approvals-handler.js";
|
|
|
|
const baseRequest = {
|
|
id: "9f1c7d5d-b1fb-46ef-ac45-662723b65bb7",
|
|
request: {
|
|
command: "npm view diver name version description",
|
|
agentId: "main",
|
|
sessionKey: "agent:main:telegram:group:-1003841603622:topic:928",
|
|
turnSourceChannel: "telegram",
|
|
turnSourceTo: "-1003841603622",
|
|
turnSourceThreadId: "928",
|
|
turnSourceAccountId: "default",
|
|
},
|
|
createdAtMs: 1000,
|
|
expiresAtMs: 61_000,
|
|
};
|
|
|
|
const pluginRequest = {
|
|
id: "plugin:9f1c7d5d-b1fb-46ef-ac45-662723b65bb7",
|
|
request: {
|
|
title: "Plugin Approval Required",
|
|
description: "Allow plugin access",
|
|
pluginId: "git-tools",
|
|
agentId: "main",
|
|
sessionKey: "agent:main:telegram:group:-1003841603622:topic:928",
|
|
turnSourceChannel: "telegram",
|
|
turnSourceTo: "-1003841603622",
|
|
turnSourceThreadId: "928",
|
|
turnSourceAccountId: "default",
|
|
},
|
|
createdAtMs: 1000,
|
|
expiresAtMs: 61_000,
|
|
};
|
|
|
|
function createHandler(cfg: OpenClawConfig, accountId = "default") {
|
|
const sendTyping = vi.fn().mockResolvedValue({ ok: true });
|
|
const sendMessage = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ messageId: "m1", chatId: "-1003841603622" })
|
|
.mockResolvedValue({ messageId: "m2", chatId: "8460800771" });
|
|
const editReplyMarkup = vi.fn().mockResolvedValue({ ok: true });
|
|
const handler = new TelegramExecApprovalHandler(
|
|
{
|
|
token: "tg-token",
|
|
accountId,
|
|
cfg,
|
|
},
|
|
{
|
|
nowMs: () => 1000,
|
|
sendTyping,
|
|
sendMessage,
|
|
editReplyMarkup,
|
|
},
|
|
);
|
|
return { handler, sendTyping, sendMessage, editReplyMarkup };
|
|
}
|
|
|
|
describe("TelegramExecApprovalHandler", () => {
|
|
it("sends approval prompts to the originating telegram topic when target=channel", async () => {
|
|
const cfg = {
|
|
channels: {
|
|
telegram: {
|
|
execApprovals: {
|
|
enabled: true,
|
|
approvers: ["8460800771"],
|
|
target: "channel",
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const { handler, sendTyping, sendMessage } = createHandler(cfg);
|
|
|
|
await handler.handleRequested(baseRequest);
|
|
|
|
expect(sendTyping).toHaveBeenCalledWith(
|
|
"-1003841603622",
|
|
expect.objectContaining({
|
|
accountId: "default",
|
|
messageThreadId: 928,
|
|
}),
|
|
);
|
|
expect(sendMessage).toHaveBeenCalledWith(
|
|
"-1003841603622",
|
|
expect.stringContaining("/approve 9f1c7d5d-b1fb-46ef-ac45-662723b65bb7 allow-once"),
|
|
expect.objectContaining({
|
|
accountId: "default",
|
|
messageThreadId: 928,
|
|
buttons: [
|
|
[
|
|
{
|
|
text: "Allow Once",
|
|
callback_data: "/approve 9f1c7d5d-b1fb-46ef-ac45-662723b65bb7 allow-once",
|
|
style: "success",
|
|
},
|
|
{
|
|
text: "Allow Always",
|
|
callback_data: "/approve 9f1c7d5d-b1fb-46ef-ac45-662723b65bb7 always",
|
|
style: "primary",
|
|
},
|
|
{
|
|
text: "Deny",
|
|
callback_data: "/approve 9f1c7d5d-b1fb-46ef-ac45-662723b65bb7 deny",
|
|
style: "danger",
|
|
},
|
|
],
|
|
],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("falls back to approver DMs when channel routing is unavailable", async () => {
|
|
const cfg = {
|
|
channels: {
|
|
telegram: {
|
|
execApprovals: {
|
|
enabled: true,
|
|
approvers: ["111", "222"],
|
|
target: "channel",
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const { handler, sendMessage } = createHandler(cfg);
|
|
|
|
await handler.handleRequested({
|
|
...baseRequest,
|
|
request: {
|
|
...baseRequest.request,
|
|
turnSourceChannel: "slack",
|
|
turnSourceTo: "U1",
|
|
turnSourceAccountId: null,
|
|
turnSourceThreadId: null,
|
|
},
|
|
});
|
|
|
|
expect(sendMessage).toHaveBeenCalledTimes(2);
|
|
expect(sendMessage.mock.calls.map((call) => call[0])).toEqual(["111", "222"]);
|
|
});
|
|
|
|
it("clears buttons from tracked approval messages when resolved", async () => {
|
|
const cfg = {
|
|
channels: {
|
|
telegram: {
|
|
execApprovals: {
|
|
enabled: true,
|
|
approvers: ["8460800771"],
|
|
target: "both",
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const { handler, editReplyMarkup } = createHandler(cfg);
|
|
|
|
await handler.handleRequested(baseRequest);
|
|
await handler.handleResolved({
|
|
id: baseRequest.id,
|
|
decision: "allow-once",
|
|
resolvedBy: "telegram:8460800771",
|
|
ts: 2000,
|
|
});
|
|
|
|
expect(editReplyMarkup).toHaveBeenCalled();
|
|
expect(editReplyMarkup).toHaveBeenCalledWith(
|
|
"-1003841603622",
|
|
"m1",
|
|
[],
|
|
expect.objectContaining({
|
|
accountId: "default",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("delivers plugin approvals through the shared native delivery planner", async () => {
|
|
const cfg = {
|
|
channels: {
|
|
telegram: {
|
|
execApprovals: {
|
|
enabled: true,
|
|
approvers: ["8460800771"],
|
|
target: "dm",
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const { handler, sendMessage } = createHandler(cfg);
|
|
|
|
await handler.handleRequested(pluginRequest);
|
|
|
|
const [chatId, text, options] = sendMessage.mock.calls[0] ?? [];
|
|
expect(chatId).toBe("8460800771");
|
|
expect(text).toContain("Plugin approval required");
|
|
expect(options).toEqual(
|
|
expect.objectContaining({
|
|
accountId: "default",
|
|
buttons: expect.arrayContaining([
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
callback_data: "/approve plugin:9f1c7d5d-b1fb-46ef-ac45-662723b65bb7 allow-once",
|
|
}),
|
|
]),
|
|
]),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("does not deliver plugin approvals for a different Telegram account", async () => {
|
|
const cfg = {
|
|
channels: {
|
|
telegram: {
|
|
execApprovals: {
|
|
enabled: true,
|
|
approvers: ["8460800771"],
|
|
target: "dm",
|
|
},
|
|
accounts: {
|
|
secondary: {
|
|
execApprovals: {
|
|
enabled: true,
|
|
approvers: ["999"],
|
|
target: "dm",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const { handler, sendMessage } = createHandler(cfg);
|
|
|
|
await handler.handleRequested({
|
|
...pluginRequest,
|
|
request: {
|
|
...pluginRequest.request,
|
|
turnSourceAccountId: "secondary",
|
|
},
|
|
});
|
|
|
|
expect(sendMessage).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("falls back to the session-bound Telegram account when turn source account is missing", async () => {
|
|
const sessionStoreDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-tg-approvals-"));
|
|
const storePath = path.join(sessionStoreDir, "sessions.json");
|
|
try {
|
|
await updateSessionStore(storePath, (store) => {
|
|
store[baseRequest.request.sessionKey] = {
|
|
sessionId: "session-secondary",
|
|
updatedAt: Date.now(),
|
|
deliveryContext: {
|
|
channel: "telegram",
|
|
to: "-1003841603622",
|
|
accountId: "secondary",
|
|
threadId: 928,
|
|
},
|
|
};
|
|
});
|
|
|
|
const cfg = {
|
|
session: { store: storePath },
|
|
channels: {
|
|
telegram: {
|
|
execApprovals: {
|
|
enabled: true,
|
|
approvers: ["8460800771"],
|
|
target: "channel",
|
|
},
|
|
accounts: {
|
|
secondary: {
|
|
execApprovals: {
|
|
enabled: true,
|
|
approvers: ["999"],
|
|
target: "channel",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const defaultHandler = createHandler(cfg, "default");
|
|
const secondaryHandler = createHandler(cfg, "secondary");
|
|
const request = {
|
|
...baseRequest,
|
|
request: {
|
|
...baseRequest.request,
|
|
turnSourceAccountId: null,
|
|
},
|
|
};
|
|
|
|
await defaultHandler.handler.handleRequested(request);
|
|
await secondaryHandler.handler.handleRequested(request);
|
|
|
|
expect(defaultHandler.sendMessage).not.toHaveBeenCalled();
|
|
expect(secondaryHandler.sendMessage).toHaveBeenCalledWith(
|
|
"-1003841603622",
|
|
expect.stringContaining("/approve 9f1c7d5d-b1fb-46ef-ac45-662723b65bb7 allow-once"),
|
|
expect.objectContaining({
|
|
accountId: "secondary",
|
|
messageThreadId: 928,
|
|
}),
|
|
);
|
|
} finally {
|
|
await fs.rm(sessionStoreDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|